feat(kol): add selection-based AI optimize for prompt editor
Trigger an AI optimize panel from a text selection in the KOL prompt editor, accept a user instruction, and stream a preview before replacing the selected fragment. Extend the shared editor AI assist panel with boundary-aware placement so it can be hosted inside scoped surfaces, and update the backend optimize prompt to honor the user instruction. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -145,6 +145,7 @@ type ImageContextMenuAction =
|
||||
type ImagePickerAction = "insert" | "replace";
|
||||
|
||||
type AiOptimizeStatus = "idle" | "generating" | "completed" | "error";
|
||||
type EditorAiAssistPlacement = "bottom" | "top";
|
||||
|
||||
type TableContextMenuState = {
|
||||
open: boolean;
|
||||
@@ -175,6 +176,7 @@ type AiOptimizePanelState = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
placement: EditorAiAssistPlacement;
|
||||
from: number;
|
||||
to: number;
|
||||
selectedText: string;
|
||||
@@ -255,6 +257,7 @@ function createDefaultAiOptimizePanelState(): AiOptimizePanelState {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
placement: "bottom",
|
||||
from: 0,
|
||||
to: 0,
|
||||
selectedText: "",
|
||||
@@ -941,20 +944,29 @@ function resolveTextSelectionSnapshot(ctx: Ctx): TextSelectionSnapshot | null {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveAiOptimizePanelFrame(anchorRect: DOMRect): { x: number; y: number; width: number } {
|
||||
function resolveAiOptimizePanelFrame(anchorRect: DOMRect): { x: number; y: number; width: number; placement: EditorAiAssistPlacement } {
|
||||
const viewportPadding = 16;
|
||||
const panelPadding = 24;
|
||||
const maxWidth = Math.max(240, window.innerWidth - viewportPadding * 2);
|
||||
const preferredWidth = 1120;
|
||||
|
||||
const surfaceRect = surfaceRef.value?.getBoundingClientRect();
|
||||
const boundary = surfaceRect ?? new DOMRect(0, 0, window.innerWidth, window.innerHeight);
|
||||
const widthFromSurface = surfaceRect ? Math.max(240, surfaceRect.width - panelPadding * 2) : preferredWidth;
|
||||
const width = Math.min(maxWidth, preferredWidth, widthFromSurface);
|
||||
const availableBelow = boundary.bottom - anchorRect.bottom - AI_OPTIMIZE_PANEL_GAP - viewportPadding;
|
||||
const availableAbove = anchorRect.top - boundary.top - AI_OPTIMIZE_PANEL_GAP - viewportPadding;
|
||||
const placement: EditorAiAssistPlacement =
|
||||
availableBelow >= 320 || availableBelow >= availableAbove ? "bottom" : "top";
|
||||
|
||||
return {
|
||||
x: anchorRect.left + anchorRect.width / 2,
|
||||
y: Math.max(viewportPadding, anchorRect.bottom + AI_OPTIMIZE_PANEL_GAP),
|
||||
y:
|
||||
placement === "top"
|
||||
? Math.min(boundary.bottom - viewportPadding, anchorRect.top - AI_OPTIMIZE_PANEL_GAP)
|
||||
: Math.max(boundary.top + viewportPadding, anchorRect.bottom + AI_OPTIMIZE_PANEL_GAP),
|
||||
width,
|
||||
placement,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -981,6 +993,7 @@ function openAiOptimizePanel(ctx: Ctx): void {
|
||||
x: frame.x,
|
||||
y: frame.y,
|
||||
width: frame.width,
|
||||
placement: frame.placement,
|
||||
from: snapshot.from,
|
||||
to: snapshot.to,
|
||||
selectedText: snapshot.text,
|
||||
@@ -1975,6 +1988,8 @@ function runTableContextAction(action: TableContextMenuAction): void {
|
||||
:x="aiOptimizePanel.x"
|
||||
:y="aiOptimizePanel.y"
|
||||
:width="aiOptimizePanel.width"
|
||||
:placement="aiOptimizePanel.placement"
|
||||
:boundary-element="surfaceRef"
|
||||
:status="aiOptimizeStatus"
|
||||
:streaming="aiOptimizeStreaming"
|
||||
:has-preview="aiOptimizeHasPreview"
|
||||
|
||||
@@ -3,11 +3,21 @@ import { CheckOutlined, CloseOutlined, DeleteOutlined, LoadingOutlined, RedoOutl
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch, type CSSProperties } from "vue";
|
||||
|
||||
type EditorAiAssistStatus = "idle" | "generating" | "completed" | "error";
|
||||
type EditorAiAssistBoundary = {
|
||||
left: number;
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
};
|
||||
type EditorAiAssistPlacement = "bottom" | "top";
|
||||
|
||||
const props = defineProps<{
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
boundary?: EditorAiAssistBoundary | null;
|
||||
boundaryElement?: HTMLElement | null;
|
||||
placement?: EditorAiAssistPlacement;
|
||||
prompt: string;
|
||||
status: EditorAiAssistStatus;
|
||||
streaming: boolean;
|
||||
@@ -34,14 +44,24 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const IDLE_MAX_WIDTH = 760;
|
||||
const PANEL_OFFSET = 16;
|
||||
const MIN_PANEL_WIDTH = 240;
|
||||
const RESULT_CHROME_HEIGHT = 176;
|
||||
|
||||
const panelRef = ref<HTMLElement | null>(null);
|
||||
const resultContentRef = ref<HTMLElement | null>(null);
|
||||
const promptInputRef = ref<HTMLInputElement | null>(null);
|
||||
const frame = ref({
|
||||
x: props.x,
|
||||
y: props.y,
|
||||
width: props.width,
|
||||
maxHeight: 480,
|
||||
resultMaxHeight: 300,
|
||||
});
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
let mutationObserver: MutationObserver | null = null;
|
||||
let syncFrameId = 0;
|
||||
let scrollFrameId = 0;
|
||||
|
||||
const promptValue = computed({
|
||||
get: () => props.prompt,
|
||||
@@ -58,12 +78,86 @@ const panelStyle = computed<CSSProperties>(() => ({
|
||||
left: `${frame.value.x}px`,
|
||||
top: `${frame.value.y}px`,
|
||||
width: `${frame.value.width}px`,
|
||||
}));
|
||||
maxHeight: `${frame.value.maxHeight}px`,
|
||||
"--editor-ai-assist-result-max-height": `${frame.value.resultMaxHeight}px`,
|
||||
} as CSSProperties));
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
function normalizeBoundary(rawBoundary: EditorAiAssistBoundary | null | undefined): EditorAiAssistBoundary {
|
||||
const viewportBoundary = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: window.innerWidth,
|
||||
bottom: window.innerHeight,
|
||||
};
|
||||
if (!rawBoundary) {
|
||||
return viewportBoundary;
|
||||
}
|
||||
|
||||
const left = clamp(rawBoundary.left, 0, window.innerWidth);
|
||||
const top = clamp(rawBoundary.top, 0, window.innerHeight);
|
||||
const right = clamp(rawBoundary.right, left, window.innerWidth);
|
||||
const bottom = clamp(rawBoundary.bottom, top, window.innerHeight);
|
||||
|
||||
if (right - left < 1 || bottom - top < 1) {
|
||||
return viewportBoundary;
|
||||
}
|
||||
|
||||
return { left, top, right, bottom };
|
||||
}
|
||||
|
||||
function resolveBoundary(): EditorAiAssistBoundary {
|
||||
if (props.boundaryElement) {
|
||||
const rect = props.boundaryElement.getBoundingClientRect();
|
||||
return normalizeBoundary({
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
right: rect.right,
|
||||
bottom: rect.bottom,
|
||||
});
|
||||
}
|
||||
return normalizeBoundary(props.boundary);
|
||||
}
|
||||
|
||||
function updateFrame(nextFrame: typeof frame.value): void {
|
||||
const current = frame.value;
|
||||
const hasChanged =
|
||||
Math.abs(current.x - nextFrame.x) > 0.5 ||
|
||||
Math.abs(current.y - nextFrame.y) > 0.5 ||
|
||||
Math.abs(current.width - nextFrame.width) > 0.5 ||
|
||||
Math.abs(current.maxHeight - nextFrame.maxHeight) > 0.5 ||
|
||||
Math.abs(current.resultMaxHeight - nextFrame.resultMaxHeight) > 0.5;
|
||||
|
||||
if (hasChanged) {
|
||||
frame.value = nextFrame;
|
||||
}
|
||||
}
|
||||
|
||||
function scrollResultContentToLatest(): void {
|
||||
if (!(props.streaming || props.status === "generating")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (scrollFrameId) {
|
||||
cancelAnimationFrame(scrollFrameId);
|
||||
}
|
||||
scrollFrameId = requestAnimationFrame(() => {
|
||||
scrollFrameId = 0;
|
||||
const resultContent = resultContentRef.value;
|
||||
if (resultContent) {
|
||||
resultContent.scrollTop = resultContent.scrollHeight;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleResultContentChanged(): void {
|
||||
schedulePanelFrameSync();
|
||||
scrollResultContentToLatest();
|
||||
}
|
||||
|
||||
function syncPanelFrame(): void {
|
||||
void nextTick(() => {
|
||||
const panel = panelRef.value;
|
||||
@@ -71,17 +165,55 @@ function syncPanelFrame(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
const offset = 16;
|
||||
const maxWidth = Math.max(240, window.innerWidth - offset * 2);
|
||||
const boundary = resolveBoundary();
|
||||
const availableWidth = Math.max(0, boundary.right - boundary.left - PANEL_OFFSET * 2);
|
||||
const maxWidth = Math.max(160, availableWidth);
|
||||
const minWidth = Math.min(MIN_PANEL_WIDTH, maxWidth);
|
||||
const width =
|
||||
props.status === "idle"
|
||||
? Math.min(props.width, maxWidth, IDLE_MAX_WIDTH)
|
||||
: Math.min(props.width, maxWidth);
|
||||
frame.value = {
|
||||
x: clamp(props.x - width / 2, offset, Math.max(offset, window.innerWidth - width - offset)),
|
||||
y: clamp(props.y, offset, Math.max(offset, window.innerHeight - panel.offsetHeight - offset)),
|
||||
? clamp(Math.min(props.width, IDLE_MAX_WIDTH), minWidth, maxWidth)
|
||||
: clamp(props.width, minWidth, maxWidth);
|
||||
const placement = props.placement ?? "bottom";
|
||||
const anchorY = clamp(props.y, boundary.top + PANEL_OFFSET, boundary.bottom - PANEL_OFFSET);
|
||||
const availableHeight = Math.max(
|
||||
120,
|
||||
placement === "top"
|
||||
? anchorY - boundary.top - PANEL_OFFSET
|
||||
: boundary.bottom - anchorY - PANEL_OFFSET,
|
||||
);
|
||||
const panelHeight = Math.min(panel.offsetHeight || availableHeight, availableHeight);
|
||||
const resultMaxHeight = Math.max(
|
||||
48,
|
||||
availableHeight - RESULT_CHROME_HEIGHT,
|
||||
);
|
||||
const panelTop = placement === "top" ? anchorY - panelHeight : anchorY;
|
||||
|
||||
updateFrame({
|
||||
x: clamp(
|
||||
props.x - width / 2,
|
||||
boundary.left + PANEL_OFFSET,
|
||||
Math.max(boundary.left + PANEL_OFFSET, boundary.right - width - PANEL_OFFSET),
|
||||
),
|
||||
y: clamp(
|
||||
panelTop,
|
||||
boundary.top + PANEL_OFFSET,
|
||||
Math.max(boundary.top + PANEL_OFFSET, boundary.bottom - panelHeight - PANEL_OFFSET),
|
||||
),
|
||||
width,
|
||||
};
|
||||
maxHeight: availableHeight,
|
||||
resultMaxHeight,
|
||||
});
|
||||
scrollResultContentToLatest();
|
||||
});
|
||||
}
|
||||
|
||||
function schedulePanelFrameSync(): void {
|
||||
if (syncFrameId) {
|
||||
cancelAnimationFrame(syncFrameId);
|
||||
}
|
||||
syncFrameId = requestAnimationFrame(() => {
|
||||
syncFrameId = 0;
|
||||
syncPanelFrame();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -100,6 +232,23 @@ function handleWindowResize(): void {
|
||||
syncPanelFrame();
|
||||
}
|
||||
|
||||
function refreshResultContentObserver(): void {
|
||||
void nextTick(() => {
|
||||
mutationObserver?.disconnect();
|
||||
const resultContent = resultContentRef.value;
|
||||
if (!resultContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
mutationObserver?.observe(resultContent, {
|
||||
childList: true,
|
||||
characterData: true,
|
||||
subtree: true,
|
||||
});
|
||||
scrollResultContentToLatest();
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => ({
|
||||
x: props.x,
|
||||
@@ -108,9 +257,13 @@ watch(
|
||||
status: props.status,
|
||||
hasPreview: props.hasPreview,
|
||||
error: props.error,
|
||||
boundary: props.boundary,
|
||||
boundaryElement: props.boundaryElement,
|
||||
placement: props.placement,
|
||||
}),
|
||||
() => {
|
||||
syncPanelFrame();
|
||||
refreshResultContentObserver();
|
||||
focusPromptInput();
|
||||
},
|
||||
{
|
||||
@@ -121,12 +274,32 @@ watch(
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("resize", handleWindowResize);
|
||||
mutationObserver = new MutationObserver(handleResultContentChanged);
|
||||
if ("ResizeObserver" in window) {
|
||||
resizeObserver = new ResizeObserver(schedulePanelFrameSync);
|
||||
if (panelRef.value) {
|
||||
resizeObserver.observe(panelRef.value);
|
||||
}
|
||||
}
|
||||
refreshResultContentObserver();
|
||||
syncPanelFrame();
|
||||
focusPromptInput();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", handleWindowResize);
|
||||
mutationObserver?.disconnect();
|
||||
mutationObserver = null;
|
||||
resizeObserver?.disconnect();
|
||||
resizeObserver = null;
|
||||
if (syncFrameId) {
|
||||
cancelAnimationFrame(syncFrameId);
|
||||
syncFrameId = 0;
|
||||
}
|
||||
if (scrollFrameId) {
|
||||
cancelAnimationFrame(scrollFrameId);
|
||||
scrollFrameId = 0;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -179,7 +352,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
<div v-else class="editor-ai-assist__result">
|
||||
<div class="editor-ai-assist__result-box">
|
||||
<div class="editor-ai-assist__result-content">
|
||||
<div ref="resultContentRef" class="editor-ai-assist__result-content">
|
||||
<slot v-if="hasPreview" name="preview"></slot>
|
||||
<div v-else-if="streaming" class="editor-ai-assist__placeholder">
|
||||
<LoadingOutlined />
|
||||
@@ -269,22 +442,38 @@ onBeforeUnmount(() => {
|
||||
position: fixed;
|
||||
z-index: 1300;
|
||||
max-width: min(1120px, calc(100vw - 32px));
|
||||
/* Removed overflow: hidden to prevent shadow clipping */
|
||||
}
|
||||
|
||||
.editor-ai-assist__shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: inherit;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.editor-ai-assist__bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 999px;
|
||||
background: #ffffff;
|
||||
padding: 8px 8px 8px 16px;
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border: 1px solid rgba(17, 24, 39, 0.08);
|
||||
box-shadow:
|
||||
0 8px 32px rgb(17 17 17 / 28%), 0 0 0 1px rgba(15, 23, 42, 0.05);
|
||||
0 16px 48px -4px rgba(17, 24, 39, 0.12),
|
||||
0 6px 20px -4px rgba(17, 24, 39, 0.08);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.editor-ai-assist__bar:focus-within {
|
||||
border-color: rgba(37, 99, 235, 0.3);
|
||||
box-shadow:
|
||||
0 12px 32px -4px rgba(37, 99, 235, 0.12),
|
||||
0 4px 16px -4px rgba(37, 99, 235, 0.06),
|
||||
0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.editor-ai-assist__bar-icon {
|
||||
@@ -295,8 +484,8 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.editor-ai-assist__bar-label {
|
||||
color: #101828;
|
||||
font-size: 15px;
|
||||
color: #111827;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -307,44 +496,46 @@ onBeforeUnmount(() => {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
color: #344054;
|
||||
color: #111827;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.editor-ai-assist__bar-input::placeholder {
|
||||
color: #98a2b3;
|
||||
color: #9ca3af;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.editor-ai-assist__bar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.editor-ai-assist__divider {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
margin: 0 4px;
|
||||
background: #eaecf0;
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.editor-ai-assist__icon-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: #667085;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.editor-ai-assist__icon-button:hover {
|
||||
background: #f2f4f7;
|
||||
color: #344054;
|
||||
background: #f3f4f6;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.editor-ai-assist__send-button {
|
||||
@@ -354,42 +545,62 @@ onBeforeUnmount(() => {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: #245bdb;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 8px rgba(36, 91, 219, 0.28);
|
||||
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.25);
|
||||
}
|
||||
|
||||
.editor-ai-assist__send-button:hover {
|
||||
background: #1d4fc4;
|
||||
background: linear-gradient(135deg, #1d4ed8 0%, #1e3a8a 100%);
|
||||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.35);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.editor-ai-assist__send-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.editor-ai-assist__result {
|
||||
width: 100%;
|
||||
max-height: inherit;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
background: #ffffff;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border: 1px solid rgba(17, 24, 39, 0.08);
|
||||
box-shadow:
|
||||
0 8px 32px rgb(17 17 17 / 28%), 0 0 0 1px rgba(15, 23, 42, 0.05);
|
||||
0 16px 48px -4px rgba(17, 24, 39, 0.12),
|
||||
0 6px 20px -4px rgba(17, 24, 39, 0.08);
|
||||
}
|
||||
|
||||
.editor-ai-assist__result-box {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
margin: 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #eef2f6;
|
||||
border: 1px solid rgba(17, 24, 39, 0.06);
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
background: #f9fafb;
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.editor-ai-assist__result-content {
|
||||
min-height: 80px;
|
||||
max-height: 300px;
|
||||
flex: 1 1 auto;
|
||||
min-height: min(80px, var(--editor-ai-assist-result-max-height, 80px));
|
||||
max-height: var(--editor-ai-assist-result-max-height, 300px);
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
color: #101828;
|
||||
font-size: 15px;
|
||||
color: #111827;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@@ -397,7 +608,7 @@ onBeforeUnmount(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #667085;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.editor-ai-assist__placeholder--error {
|
||||
@@ -405,13 +616,14 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.editor-ai-assist__result-footer {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 16px;
|
||||
border-top: 1px solid rgba(226, 232, 240, 0.6);
|
||||
border-top: 1px solid rgba(229, 231, 235, 0.8);
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
color: #94a3b8;
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -422,7 +634,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.editor-ai-assist__result-badges :deep(svg) {
|
||||
color: #52c41a;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.editor-ai-assist__thumb {
|
||||
@@ -435,6 +647,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.editor-ai-assist__toolbar {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 16px 16px;
|
||||
@@ -445,7 +658,7 @@ onBeforeUnmount(() => {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
color: #64748b;
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@@ -459,10 +672,11 @@ onBeforeUnmount(() => {
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.editor-ai-assist__back-button:hover {
|
||||
color: #334155;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.editor-ai-assist__history {
|
||||
@@ -487,9 +701,16 @@ onBeforeUnmount(() => {
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
padding: 4px 4px 4px 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 20px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.02);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.editor-ai-assist__continue:focus-within {
|
||||
border-color: rgba(37, 99, 235, 0.4);
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.02), 0 0 0 2px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.editor-ai-assist__continue input {
|
||||
@@ -497,7 +718,7 @@ onBeforeUnmount(() => {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
color: #334155;
|
||||
color: #111827;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@@ -508,10 +729,16 @@ onBeforeUnmount(() => {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: #e8efff;
|
||||
color: #245bdb;
|
||||
border-radius: 8px;
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.editor-ai-assist__continue-send:hover {
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.editor-ai-assist__toolbar-actions {
|
||||
@@ -525,27 +752,35 @@ onBeforeUnmount(() => {
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 14px;
|
||||
border: none;
|
||||
border: 1px solid rgba(17, 24, 39, 0.05);
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: #475569;
|
||||
background: #ffffff;
|
||||
color: #4b5563;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.editor-ai-assist__toolbar-button:hover {
|
||||
background: #f1f5f9;
|
||||
background: #f9fafb;
|
||||
color: #111827;
|
||||
border-color: rgba(17, 24, 39, 0.1);
|
||||
}
|
||||
|
||||
.editor-ai-assist__toolbar-button--primary {
|
||||
background: #245bdb;
|
||||
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
|
||||
border: none;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.2);
|
||||
}
|
||||
|
||||
.editor-ai-assist__toolbar-button--primary:hover {
|
||||
background: #1d4fc4;
|
||||
background: linear-gradient(135deg, #1d4ed8 0%, #1e3a8a 100%);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 4px 10px rgba(37, 99, 235, 0.3);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -555,7 +790,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
.editor-ai-assist__bar {
|
||||
flex-wrap: wrap;
|
||||
border-radius: 24px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.editor-ai-assist__bar-input {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, reactive, ref, watch } from "vue";
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/vue-query";
|
||||
import { message } from "ant-design-vue";
|
||||
@@ -9,6 +9,7 @@ import KolVariablePanel from "./KolVariablePanel.vue";
|
||||
import KolVariableConfig from "./KolVariableConfig.vue";
|
||||
import KolPromptEditArea from "./KolPromptEditArea.vue";
|
||||
import KolDynamicForm from "./KolDynamicForm.vue";
|
||||
import EditorAiAssistPanel from "@/components/editor-common/EditorAiAssistPanel.vue";
|
||||
import { kolManageApi } from "@/lib/api";
|
||||
import type { KolAssistRequest, KolAssistStreamEvent, KolCardConfig, KolVariableDefinition } from "@geo/shared-types";
|
||||
import { formatError } from "@/lib/errors";
|
||||
@@ -24,6 +25,30 @@ const props = defineProps<{
|
||||
promptId: number;
|
||||
}>();
|
||||
|
||||
type AiOptimizeStatus = "idle" | "generating" | "completed" | "error";
|
||||
type EditorAiAssistPlacement = "bottom" | "top";
|
||||
type EditorAiAssistBoundary = {
|
||||
left: number;
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
};
|
||||
|
||||
type PromptSelectionAiSnapshot = {
|
||||
start: number;
|
||||
end: number;
|
||||
selectedText: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
placement: EditorAiAssistPlacement;
|
||||
boundary: EditorAiAssistBoundary;
|
||||
};
|
||||
|
||||
type PromptSelectionAiPanelState = PromptSelectionAiSnapshot & {
|
||||
open: boolean;
|
||||
};
|
||||
|
||||
const emit = defineEmits<{
|
||||
back: [];
|
||||
}>();
|
||||
@@ -31,11 +56,30 @@ const emit = defineEmits<{
|
||||
const { t } = useI18n();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
function createDefaultPromptSelectionAiPanelState(): PromptSelectionAiPanelState {
|
||||
return {
|
||||
open: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
placement: "bottom",
|
||||
boundary: { left: 0, top: 0, right: 0, bottom: 0 },
|
||||
start: 0,
|
||||
end: 0,
|
||||
selectedText: "",
|
||||
};
|
||||
}
|
||||
|
||||
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>({});
|
||||
const variableConfigRef = ref<{
|
||||
focusVariable: (key: string, options?: { focusInput?: boolean }) => void;
|
||||
@@ -60,6 +104,18 @@ const platformOptions = computed(() => {
|
||||
}
|
||||
return options;
|
||||
});
|
||||
const selectionAiStreaming = computed(() => selectionAiStatus.value === "generating");
|
||||
const selectionAiHasPreview = computed(() => selectionAiPreview.value.trim().length > 0);
|
||||
const selectionAiUiText = computed(() => ({
|
||||
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"),
|
||||
}));
|
||||
|
||||
watch(
|
||||
() => promptDetail.value,
|
||||
@@ -107,6 +163,7 @@ const saveMutation = useMutation({
|
||||
});
|
||||
|
||||
let aiAssistAbortController: AbortController | null = null;
|
||||
let selectionAiAbortController: AbortController | null = null;
|
||||
|
||||
function applyAssistResult(event: KolAssistStreamEvent) {
|
||||
const nextContent = (event.content ?? "").trim();
|
||||
@@ -212,14 +269,155 @@ async function handleAiGenerate(description: string) {
|
||||
});
|
||||
}
|
||||
|
||||
async function handleAiOptimize() {
|
||||
function resetSelectionAiOutput() {
|
||||
selectionAiPreview.value = "";
|
||||
selectionAiError.value = "";
|
||||
selectionAiStatus.value = "idle";
|
||||
}
|
||||
|
||||
function stopSelectionAiStream() {
|
||||
selectionAiAbortController?.abort();
|
||||
selectionAiAbortController = null;
|
||||
|
||||
if (selectionAiStatus.value === "generating") {
|
||||
selectionAiStatus.value = selectionAiHasPreview.value ? "completed" : "idle";
|
||||
selectionAiError.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
function closeSelectionAiPanel() {
|
||||
stopSelectionAiStream();
|
||||
selectionAiPanel.value = createDefaultPromptSelectionAiPanelState();
|
||||
selectionAiInstruction.value = "";
|
||||
resetSelectionAiOutput();
|
||||
}
|
||||
|
||||
function handleOpenSelectionAiPanel(snapshot: PromptSelectionAiSnapshot) {
|
||||
stopAiAssistStream();
|
||||
stopSelectionAiStream();
|
||||
selectionAiPanel.value = {
|
||||
open: true,
|
||||
...snapshot,
|
||||
};
|
||||
selectionAiInstruction.value = "";
|
||||
resetSelectionAiOutput();
|
||||
}
|
||||
|
||||
function handleSelectionAiStreamEvent(event: KolAssistStreamEvent) {
|
||||
if (event.type === "start") {
|
||||
selectionAiStatus.value = "generating";
|
||||
selectionAiError.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === "completed") {
|
||||
selectionAiStatus.value = "completed";
|
||||
selectionAiError.value = "";
|
||||
if (typeof event.content === "string") {
|
||||
selectionAiPreview.value = event.content;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
if (!selectionAiHasPreview.value) {
|
||||
message.error(selectionAiError.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function generateSelectionAiPreview() {
|
||||
if (!selectionAiPanel.value.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const instruction = selectionAiInstruction.value.trim();
|
||||
if (!instruction) {
|
||||
message.warning(t("article.editor.aiOptimize.messages.promptRequired"));
|
||||
return;
|
||||
}
|
||||
|
||||
stopSelectionAiStream();
|
||||
selectionAiPreview.value = "";
|
||||
selectionAiError.value = "";
|
||||
selectionAiStatus.value = "generating";
|
||||
|
||||
const syncedVariables = syncVariablesFromContent();
|
||||
await runAiAssistStream({
|
||||
const controller = new AbortController();
|
||||
selectionAiAbortController = controller;
|
||||
|
||||
try {
|
||||
await kolManageApi.streamAssist(
|
||||
{
|
||||
mode: "optimize",
|
||||
current_content: content.value,
|
||||
description: instruction,
|
||||
current_content: selectionAiPanel.value.selectedText,
|
||||
schema: { variables: syncedVariables },
|
||||
prompt_id: props.promptId,
|
||||
});
|
||||
},
|
||||
{
|
||||
onEvent: handleSelectionAiStreamEvent,
|
||||
},
|
||||
controller.signal,
|
||||
);
|
||||
|
||||
if (!controller.signal.aborted && selectionAiStatus.value === "generating") {
|
||||
selectionAiStatus.value = selectionAiHasPreview.value ? "completed" : "error";
|
||||
}
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectionAiStatus.value = selectionAiHasPreview.value ? "completed" : "error";
|
||||
selectionAiError.value = formatError(error);
|
||||
if (!selectionAiHasPreview.value) {
|
||||
message.error(selectionAiError.value);
|
||||
}
|
||||
} finally {
|
||||
if (selectionAiAbortController === controller) {
|
||||
selectionAiAbortController = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applySelectionAiReplacement() {
|
||||
const replacement = selectionAiPreview.value.trim();
|
||||
if (!selectionAiPanel.value.open || !replacement) {
|
||||
if (!replacement) {
|
||||
message.warning(t("article.editor.aiOptimize.messages.empty"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const target = { ...selectionAiPanel.value };
|
||||
const currentSelectedText = content.value.slice(target.start, target.end);
|
||||
if (currentSelectedText !== target.selectedText) {
|
||||
message.warning(t("article.editor.aiOptimize.messages.selectionChanged"));
|
||||
return;
|
||||
}
|
||||
|
||||
const nextContent =
|
||||
content.value.slice(0, target.start) + replacement + content.value.slice(target.end);
|
||||
content.value = nextContent;
|
||||
variables.value = syncKolVariablesWithContent(nextContent, variables.value);
|
||||
closeSelectionAiPanel();
|
||||
}
|
||||
|
||||
function buildPromptPayload(nextVariables: KolVariableDefinition[] = variables.value) {
|
||||
@@ -281,6 +479,20 @@ function handleSave() {
|
||||
saveMutation.mutate(buildPromptPayload(syncedVariables));
|
||||
}
|
||||
|
||||
function handleSaveShortcut(event: KeyboardEvent) {
|
||||
if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== "s") {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (event.repeat || saveMutation.isPending.value || publishMutation.isPending.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleSave();
|
||||
}
|
||||
|
||||
function handlePublish() {
|
||||
const syncedVariables = syncVariablesFromContent();
|
||||
if (!validateBeforeSubmit()) {
|
||||
@@ -317,8 +529,32 @@ function statusLabel(status?: string): string {
|
||||
return t("kol.manage.status.draft");
|
||||
}
|
||||
|
||||
function handleWindowPointerDown(event: PointerEvent) {
|
||||
if (!(selectionAiPanel.value.open && selectionAiStatus.value === "idle")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = event.target;
|
||||
if (
|
||||
target instanceof Element &&
|
||||
(target.closest(".editor-ai-assist") || target.closest(".prompt-selection-ai-action"))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
closeSelectionAiPanel();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("keydown", handleSaveShortcut);
|
||||
window.addEventListener("pointerdown", handleWindowPointerDown);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("keydown", handleSaveShortcut);
|
||||
window.removeEventListener("pointerdown", handleWindowPointerDown);
|
||||
stopAiAssistStream();
|
||||
stopSelectionAiStream();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -346,7 +582,12 @@ onBeforeUnmount(() => {
|
||||
<template #icon><EyeOutlined /></template>
|
||||
{{ t('kol.manage.editor.preview') }}
|
||||
</a-button>
|
||||
<a-button :loading="saveMutation.isPending.value" @click="handleSave">
|
||||
<a-button
|
||||
:loading="saveMutation.isPending.value"
|
||||
title="Ctrl/Cmd + S"
|
||||
aria-keyshortcuts="Control+S Meta+S"
|
||||
@click="handleSave"
|
||||
>
|
||||
<template #icon><SaveOutlined /></template>
|
||||
{{ t('common.save') }}
|
||||
</a-button>
|
||||
@@ -399,8 +640,9 @@ onBeforeUnmount(() => {
|
||||
v-model:content="content"
|
||||
v-model:variables="variables"
|
||||
:ai-busy="aiBusy"
|
||||
:selection-ai-open="selectionAiPanel.open"
|
||||
@ai-generate="handleAiGenerate"
|
||||
@ai-optimize="handleAiOptimize"
|
||||
@ai-optimize-selection="handleOpenSelectionAiPanel"
|
||||
@focus-variable="handleFocusVariable"
|
||||
/>
|
||||
<KolVariableConfig
|
||||
@@ -420,6 +662,37 @@ onBeforeUnmount(() => {
|
||||
v-model:modelValue="previewValues"
|
||||
/>
|
||||
</a-modal>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -547,6 +820,15 @@ onBeforeUnmount(() => {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.kol-prompt-editor__ai-preview {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
color: #111827;
|
||||
font: inherit;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.editor-meta-form {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
@@ -274,7 +274,10 @@ const enUS = {
|
||||
preview: "Preview form",
|
||||
aiGenerate: "AI Generate",
|
||||
aiOptimize: "AI Optimize",
|
||||
aiPlaceholder: "Describe the prompt effect you want, and AI will generate or optimize it...",
|
||||
aiPlaceholder: "Describe the prompt effect you want, and AI will generate it...",
|
||||
aiOptimizePromptPlaceholder: "For example: make this prompt clearer, keep variable placeholders, and reduce ambiguity.",
|
||||
aiOptimizeDisclaimer: "AI output is for reference only. Review it before replacing the selected prompt text.",
|
||||
aiOptimizeReplace: "Replace selection",
|
||||
},
|
||||
variable: {
|
||||
input: "Input",
|
||||
|
||||
@@ -274,7 +274,10 @@ const zhCN = {
|
||||
preview: "预览表单",
|
||||
aiGenerate: "AI 生成",
|
||||
aiOptimize: "AI 优化",
|
||||
aiPlaceholder: "描述你想要的 prompt 效果,AI 帮你生成 / 优化...",
|
||||
aiPlaceholder: "描述你想要的 prompt 效果,AI 帮你生成...",
|
||||
aiOptimizePromptPlaceholder: "例如:让这段提示词更清晰,保留变量占位符,减少歧义。",
|
||||
aiOptimizeDisclaimer: "AI 生成内容仅供参考,请确认后再替换选中片段。",
|
||||
aiOptimizeReplace: "替换选中内容",
|
||||
},
|
||||
variable: {
|
||||
input: "输入框",
|
||||
|
||||
@@ -419,7 +419,6 @@ function packageStatusLabel(status: string) {
|
||||
min-height: 0;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.manage-content--editor {
|
||||
|
||||
@@ -115,8 +115,13 @@ func BuildKolAssistUserPrompt(req AssistRequest) (string, error) {
|
||||
}
|
||||
schemaJSON = string(payload)
|
||||
}
|
||||
instruction := strings.TrimSpace(req.Description)
|
||||
if instruction == "" {
|
||||
instruction = "Make the prompt clearer, more complete, and easier for another AI to follow."
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"Optimize the following prompt. Keep variable placeholders intact. Current schema: %s\n\nPROMPT:\n%s",
|
||||
"Optimize the following prompt according to the instruction. Keep variable placeholders intact.\n\nInstruction:\n%s\n\nCurrent schema: %s\n\nPROMPT:\n%s",
|
||||
instruction,
|
||||
schemaJSON,
|
||||
req.CurrentContent,
|
||||
), nil
|
||||
|
||||
@@ -72,6 +72,20 @@ func TestParseKolAssistResponseSupportsStringVariables(t *testing.T) {
|
||||
require.Equal(t, "城市", result.Schema.Variables[0].Key)
|
||||
}
|
||||
|
||||
func TestBuildKolAssistUserPromptIncludesOptimizeInstruction(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
prompt, err := BuildKolAssistUserPrompt(AssistRequest{
|
||||
Mode: kolAssistModeOptimize,
|
||||
Description: "让表达更清晰,但保留变量",
|
||||
CurrentContent: "围绕 {{城市}} 写一段内容",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, prompt, "Instruction:\n让表达更清晰,但保留变量")
|
||||
require.Contains(t, prompt, "PROMPT:\n围绕 {{城市}} 写一段内容")
|
||||
}
|
||||
|
||||
func TestExtractKolAssistContentFromPartialJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user