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:
2026-04-25 22:41:40 +08:00
parent 2862aff72b
commit 2846968cf7
9 changed files with 1428 additions and 112 deletions
@@ -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"