Enhance AssistantsPage template suggestion functionality by adding control parameter to updateTemplateSuggestionState. This allows for dynamic positioning of suggestion dropdowns based on caret position, improving user experience during text input. Update relevant event handlers to pass control element for accurate suggestion placement.
This commit is contained in:
@@ -273,7 +273,8 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
const updateTemplateSuggestionState = (
|
const updateTemplateSuggestionState = (
|
||||||
field: 'prompt' | 'opener',
|
field: 'prompt' | 'opener',
|
||||||
value: string,
|
value: string,
|
||||||
caret: number | null
|
caret: number | null,
|
||||||
|
control?: HTMLTextAreaElement | HTMLInputElement | null
|
||||||
) => {
|
) => {
|
||||||
if (caret === null || caret < 0) {
|
if (caret === null || caret < 0) {
|
||||||
setTemplateSuggestion(null);
|
setTemplateSuggestion(null);
|
||||||
@@ -294,11 +295,18 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
setTemplateSuggestion(null);
|
setTemplateSuggestion(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!control) {
|
||||||
|
setTemplateSuggestion(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const anchor = getTemplateSuggestionAnchor(control, caret);
|
||||||
setTemplateSuggestion({
|
setTemplateSuggestion({
|
||||||
field,
|
field,
|
||||||
start,
|
start,
|
||||||
end: caret,
|
end: caret,
|
||||||
query: token,
|
query: token,
|
||||||
|
anchorLeft: anchor.left,
|
||||||
|
anchorTop: anchor.top,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -765,16 +773,16 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const next = e.target.value;
|
const next = e.target.value;
|
||||||
updateAssistant('prompt', next);
|
updateAssistant('prompt', next);
|
||||||
updateTemplateSuggestionState('prompt', next, e.currentTarget.selectionStart);
|
updateTemplateSuggestionState('prompt', next, e.currentTarget.selectionStart, e.currentTarget);
|
||||||
}}
|
}}
|
||||||
onKeyUp={(e) => {
|
onKeyUp={(e) => {
|
||||||
updateTemplateSuggestionState('prompt', e.currentTarget.value, e.currentTarget.selectionStart);
|
updateTemplateSuggestionState('prompt', e.currentTarget.value, e.currentTarget.selectionStart, e.currentTarget);
|
||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
updateTemplateSuggestionState('prompt', e.currentTarget.value, e.currentTarget.selectionStart);
|
updateTemplateSuggestionState('prompt', e.currentTarget.value, e.currentTarget.selectionStart, e.currentTarget);
|
||||||
}}
|
}}
|
||||||
onFocus={(e) => {
|
onFocus={(e) => {
|
||||||
updateTemplateSuggestionState('prompt', e.currentTarget.value, e.currentTarget.selectionStart);
|
updateTemplateSuggestionState('prompt', e.currentTarget.value, e.currentTarget.selectionStart, e.currentTarget);
|
||||||
}}
|
}}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
@@ -784,7 +792,13 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
placeholder="设定小助手的人设、语气、行为规范以及业务逻辑..."
|
placeholder="设定小助手的人设、语气、行为规范以及业务逻辑..."
|
||||||
/>
|
/>
|
||||||
{templateSuggestion?.field === 'prompt' && filteredSystemTemplateVariables.length > 0 && (
|
{templateSuggestion?.field === 'prompt' && filteredSystemTemplateVariables.length > 0 && (
|
||||||
<div className="absolute z-20 mt-1 w-full rounded-md border border-white/15 bg-black/95 shadow-xl backdrop-blur-md max-h-48 overflow-auto">
|
<div
|
||||||
|
className="fixed z-50 w-[320px] max-w-[calc(100vw-1rem)] rounded-md border border-white/15 bg-black/95 shadow-xl backdrop-blur-md max-h-48 overflow-auto"
|
||||||
|
style={{
|
||||||
|
left: templateSuggestion.anchorLeft,
|
||||||
|
top: templateSuggestion.anchorTop,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{filteredSystemTemplateVariables.map((item) => (
|
{filteredSystemTemplateVariables.map((item) => (
|
||||||
<button
|
<button
|
||||||
key={item.key}
|
key={item.key}
|
||||||
@@ -877,19 +891,19 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
const next = e.target.value;
|
const next = e.target.value;
|
||||||
updateAssistant('opener', next);
|
updateAssistant('opener', next);
|
||||||
if (selectedAssistant.generatedOpenerEnabled === true) return;
|
if (selectedAssistant.generatedOpenerEnabled === true) return;
|
||||||
updateTemplateSuggestionState('opener', next, e.currentTarget.selectionStart);
|
updateTemplateSuggestionState('opener', next, e.currentTarget.selectionStart, e.currentTarget);
|
||||||
}}
|
}}
|
||||||
onKeyUp={(e) => {
|
onKeyUp={(e) => {
|
||||||
if (selectedAssistant.generatedOpenerEnabled === true) return;
|
if (selectedAssistant.generatedOpenerEnabled === true) return;
|
||||||
updateTemplateSuggestionState('opener', e.currentTarget.value, e.currentTarget.selectionStart);
|
updateTemplateSuggestionState('opener', e.currentTarget.value, e.currentTarget.selectionStart, e.currentTarget);
|
||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (selectedAssistant.generatedOpenerEnabled === true) return;
|
if (selectedAssistant.generatedOpenerEnabled === true) return;
|
||||||
updateTemplateSuggestionState('opener', e.currentTarget.value, e.currentTarget.selectionStart);
|
updateTemplateSuggestionState('opener', e.currentTarget.value, e.currentTarget.selectionStart, e.currentTarget);
|
||||||
}}
|
}}
|
||||||
onFocus={(e) => {
|
onFocus={(e) => {
|
||||||
if (selectedAssistant.generatedOpenerEnabled === true) return;
|
if (selectedAssistant.generatedOpenerEnabled === true) return;
|
||||||
updateTemplateSuggestionState('opener', e.currentTarget.value, e.currentTarget.selectionStart);
|
updateTemplateSuggestionState('opener', e.currentTarget.value, e.currentTarget.selectionStart, e.currentTarget);
|
||||||
}}
|
}}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
@@ -901,7 +915,13 @@ export const AssistantsPage: React.FC = () => {
|
|||||||
className="bg-white/5 border-white/10 focus:border-primary/50 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="bg-white/5 border-white/10 focus:border-primary/50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
/>
|
/>
|
||||||
{templateSuggestion?.field === 'opener' && filteredSystemTemplateVariables.length > 0 && selectedAssistant.generatedOpenerEnabled !== true && (
|
{templateSuggestion?.field === 'opener' && filteredSystemTemplateVariables.length > 0 && selectedAssistant.generatedOpenerEnabled !== true && (
|
||||||
<div className="absolute z-20 mt-1 w-full rounded-md border border-white/15 bg-black/95 shadow-xl backdrop-blur-md max-h-48 overflow-auto">
|
<div
|
||||||
|
className="fixed z-50 w-[320px] max-w-[calc(100vw-1rem)] rounded-md border border-white/15 bg-black/95 shadow-xl backdrop-blur-md max-h-48 overflow-auto"
|
||||||
|
style={{
|
||||||
|
left: templateSuggestion.anchorLeft,
|
||||||
|
top: templateSuggestion.anchorTop,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{filteredSystemTemplateVariables.map((item) => (
|
{filteredSystemTemplateVariables.map((item) => (
|
||||||
<button
|
<button
|
||||||
key={item.key}
|
key={item.key}
|
||||||
@@ -1675,12 +1695,17 @@ type TemplateSuggestionState = {
|
|||||||
start: number;
|
start: number;
|
||||||
end: number;
|
end: number;
|
||||||
query: string;
|
query: string;
|
||||||
|
anchorLeft: number;
|
||||||
|
anchorTop: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DYNAMIC_VARIABLE_KEY_RE = /^[a-zA-Z_][a-zA-Z0-9_]{0,63}$/;
|
const DYNAMIC_VARIABLE_KEY_RE = /^[a-zA-Z_][a-zA-Z0-9_]{0,63}$/;
|
||||||
const DYNAMIC_VARIABLE_PLACEHOLDER_RE = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;
|
const DYNAMIC_VARIABLE_PLACEHOLDER_RE = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;
|
||||||
const DYNAMIC_VARIABLE_MAX_ITEMS = 30;
|
const DYNAMIC_VARIABLE_MAX_ITEMS = 30;
|
||||||
const DYNAMIC_VARIABLE_MAX_VALUE_LENGTH = 1000;
|
const DYNAMIC_VARIABLE_MAX_VALUE_LENGTH = 1000;
|
||||||
|
const TEMPLATE_SUGGESTION_PANEL_WIDTH = 320;
|
||||||
|
const TEMPLATE_SUGGESTION_PANEL_MAX_HEIGHT = 240;
|
||||||
|
const TEMPLATE_SUGGESTION_VIEWPORT_PADDING = 8;
|
||||||
const SYSTEM_DYNAMIC_VARIABLE_OPTIONS = [
|
const SYSTEM_DYNAMIC_VARIABLE_OPTIONS = [
|
||||||
{
|
{
|
||||||
key: 'system__time',
|
key: 'system__time',
|
||||||
@@ -1697,6 +1722,105 @@ const SYSTEM_DYNAMIC_VARIABLE_OPTIONS = [
|
|||||||
];
|
];
|
||||||
const SYSTEM_DYNAMIC_VARIABLE_KEY_SET = new Set(SYSTEM_DYNAMIC_VARIABLE_OPTIONS.map((item) => item.key));
|
const SYSTEM_DYNAMIC_VARIABLE_KEY_SET = new Set(SYSTEM_DYNAMIC_VARIABLE_OPTIONS.map((item) => item.key));
|
||||||
|
|
||||||
|
const clampTemplateSuggestionPosition = (left: number, top: number) => {
|
||||||
|
if (typeof window === 'undefined') return { left, top };
|
||||||
|
const maxLeft = Math.max(
|
||||||
|
TEMPLATE_SUGGESTION_VIEWPORT_PADDING,
|
||||||
|
window.innerWidth - TEMPLATE_SUGGESTION_PANEL_WIDTH - TEMPLATE_SUGGESTION_VIEWPORT_PADDING
|
||||||
|
);
|
||||||
|
const maxTop = Math.max(
|
||||||
|
TEMPLATE_SUGGESTION_VIEWPORT_PADDING,
|
||||||
|
window.innerHeight - TEMPLATE_SUGGESTION_PANEL_MAX_HEIGHT - TEMPLATE_SUGGESTION_VIEWPORT_PADDING
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
left: Math.min(Math.max(left, TEMPLATE_SUGGESTION_VIEWPORT_PADDING), maxLeft),
|
||||||
|
top: Math.min(Math.max(top, TEMPLATE_SUGGESTION_VIEWPORT_PADDING), maxTop),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTemplateSuggestionAnchor = (
|
||||||
|
control: HTMLTextAreaElement | HTMLInputElement,
|
||||||
|
caret: number | null
|
||||||
|
) => {
|
||||||
|
const rect = control.getBoundingClientRect();
|
||||||
|
const fallback = clampTemplateSuggestionPosition(rect.left + 12, rect.bottom + 8);
|
||||||
|
if (
|
||||||
|
caret === null ||
|
||||||
|
caret < 0 ||
|
||||||
|
typeof window === 'undefined' ||
|
||||||
|
typeof document === 'undefined'
|
||||||
|
) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const computedStyle = window.getComputedStyle(control);
|
||||||
|
const mirror = document.createElement('div');
|
||||||
|
const marker = document.createElement('span');
|
||||||
|
const propertiesToCopy = [
|
||||||
|
'boxSizing',
|
||||||
|
'width',
|
||||||
|
'height',
|
||||||
|
'paddingTop',
|
||||||
|
'paddingRight',
|
||||||
|
'paddingBottom',
|
||||||
|
'paddingLeft',
|
||||||
|
'borderTopWidth',
|
||||||
|
'borderRightWidth',
|
||||||
|
'borderBottomWidth',
|
||||||
|
'borderLeftWidth',
|
||||||
|
'fontFamily',
|
||||||
|
'fontSize',
|
||||||
|
'fontStyle',
|
||||||
|
'fontVariant',
|
||||||
|
'fontWeight',
|
||||||
|
'letterSpacing',
|
||||||
|
'lineHeight',
|
||||||
|
'textAlign',
|
||||||
|
'textIndent',
|
||||||
|
'textTransform',
|
||||||
|
'wordSpacing',
|
||||||
|
'tabSize',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
mirror.style.position = 'fixed';
|
||||||
|
mirror.style.left = `${rect.left}px`;
|
||||||
|
mirror.style.top = `${rect.top}px`;
|
||||||
|
mirror.style.visibility = 'hidden';
|
||||||
|
mirror.style.pointerEvents = 'none';
|
||||||
|
mirror.style.overflow = 'hidden';
|
||||||
|
mirror.style.whiteSpace = control instanceof HTMLInputElement ? 'pre' : 'pre-wrap';
|
||||||
|
mirror.style.wordBreak = 'break-word';
|
||||||
|
|
||||||
|
for (const property of propertiesToCopy) {
|
||||||
|
(mirror.style as any)[property] = (computedStyle as any)[property];
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = control.value || '';
|
||||||
|
const safeCaret = Math.min(caret, value.length);
|
||||||
|
mirror.textContent = value.slice(0, safeCaret);
|
||||||
|
marker.textContent = value.slice(safeCaret, safeCaret + 1) || '\u200b';
|
||||||
|
mirror.appendChild(marker);
|
||||||
|
document.body.appendChild(mirror);
|
||||||
|
mirror.scrollTop = control.scrollTop;
|
||||||
|
mirror.scrollLeft = control.scrollLeft;
|
||||||
|
|
||||||
|
const markerRect = marker.getBoundingClientRect();
|
||||||
|
document.body.removeChild(mirror);
|
||||||
|
|
||||||
|
if (!Number.isFinite(markerRect.left) || !Number.isFinite(markerRect.top)) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineHeightRaw = Number.parseFloat(computedStyle.lineHeight);
|
||||||
|
const fontSizeRaw = Number.parseFloat(computedStyle.fontSize);
|
||||||
|
const lineHeight = Number.isFinite(lineHeightRaw)
|
||||||
|
? lineHeightRaw
|
||||||
|
: Number.isFinite(fontSizeRaw)
|
||||||
|
? fontSizeRaw * 1.2
|
||||||
|
: 16;
|
||||||
|
return clampTemplateSuggestionPosition(markerRect.left, markerRect.top + lineHeight + 6);
|
||||||
|
};
|
||||||
|
|
||||||
const extractDynamicTemplateKeys = (text: string): string[] => {
|
const extractDynamicTemplateKeys = (text: string): string[] => {
|
||||||
if (!text) return [];
|
if (!text) return [];
|
||||||
const keys = new Set<string>();
|
const keys = new Set<string>();
|
||||||
|
|||||||
Reference in New Issue
Block a user