feat(canvas): PR #539 Agent 面板 - 合并 QAyong 的回车发送与中文 Skills 触发改进
This commit is contained in:
@@ -538,10 +538,10 @@ export function AgentChatComposer({
|
||||
setPromptHeight(clampAgentPromptHeight(promptHeight + (event.key === "ArrowUp" ? 20 : -20)));
|
||||
};
|
||||
|
||||
// 在输入值末尾检测「/关键词」打开技能候选;选中后写入稳定 token,编辑器再把它渲染为技能 chip。
|
||||
// 在输入值末尾检测「/ 或 、+ 关键词」打开技能候选:中文输入法下 "/" 会打成 "、",两者等价,且都必须紧跟行首或空白,避免中文顿号误触发。选中后写入稳定 token,编辑器再把它渲染为技能 chip。
|
||||
const handlePromptChange = (value: string) => {
|
||||
onPromptChange(value);
|
||||
const match = /(^|\s)\/([^\s/]*)$/.exec(value);
|
||||
const match = /(^|\s)[/、]([^\s/、]*)$/.exec(value);
|
||||
if (match && availableSlashSkills.length) {
|
||||
const next = { start: match.index + match[1].length, query: match[2] };
|
||||
setSlash((current) => (current && current.start === next.start && current.query === next.query ? current : next));
|
||||
@@ -553,6 +553,7 @@ export function AgentChatComposer({
|
||||
|
||||
const applySlashSkill = (skill: Skill) => {
|
||||
const token = `@[skill:${skill.skillId}] `;
|
||||
// 触发符 "/" 与 "、" 都是单字符,替换长度固定为 1。
|
||||
const next = slash ? `${prompt.slice(0, slash.start)}${token}${prompt.slice(slash.start + 1 + slash.query.length)}` : prompt ? `${prompt.replace(/\s+$/u, "")} ${token}` : token;
|
||||
setSlash(null);
|
||||
setSlashIndex(0);
|
||||
@@ -673,7 +674,7 @@ export function AgentChatComposer({
|
||||
value={prompt}
|
||||
references={composerReferences}
|
||||
includeAssetLibrary={includeAssetLibrary}
|
||||
sendOnEnter={false}
|
||||
sendOnEnter={canSubmit ? "both" : false}
|
||||
disabled={disabled}
|
||||
onChange={handlePromptChange}
|
||||
onSubmit={() => { if (canSubmit) onSubmit(); }}
|
||||
@@ -740,7 +741,12 @@ export function AgentChatComposer({
|
||||
{left}
|
||||
</div>
|
||||
<div className="agent-composer-submit flex items-center gap-2">
|
||||
<span className="agent-composer-send-hint">{canStop ? "运行中:发送即插话,下一步生效" : "Enter 换行 · ⌘/Ctrl+Enter 发送"}</span>
|
||||
{disabled ? null : (
|
||||
<span className="agent-composer-send-hint">
|
||||
<span className="agent-composer-send-hint-full">{canStop ? "运行中:发送即插话,下一步生效" : "Enter 发送 · Shift+Enter 换行"}</span>
|
||||
<span className="agent-composer-send-hint-compact">{canStop ? "运行中可插话" : "Enter 发送"}</span>
|
||||
</span>
|
||||
)}
|
||||
{canStop ? <motion.button
|
||||
type="button"
|
||||
disabled={stopping}
|
||||
@@ -760,7 +766,7 @@ export function AgentChatComposer({
|
||||
type="button"
|
||||
disabled={!canSubmit}
|
||||
aria-label={sending ? "发送中" : canStop ? "插话" : "发送"}
|
||||
title={canStop ? "插话:Agent 下一次开口时看到它" : "点击发送;⌘/Ctrl+Enter 发送"}
|
||||
title={canStop ? "插话:Agent 下一次开口时看到它" : "点击发送;Enter 或 ⌘/Ctrl+Enter 发送"}
|
||||
onClick={() => onSubmit()}
|
||||
whileHover={canSubmit && !reducedMotion ? { scale: 1.06, y: -1 } : undefined}
|
||||
whileTap={canSubmit && !reducedMotion ? { scale: 0.9, y: 1 } : undefined}
|
||||
|
||||
@@ -733,7 +733,7 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, re
|
||||
disabled={Boolean(run && connectionStatus !== "connected") || !historyHydrated || !pendingHydrated}
|
||||
sending={busy}
|
||||
running={running}
|
||||
placeholder={running ? "运行中可直接插话,会在它下一步生效" : "输入操作指导;用 @ 引用画布节点,用 / 引用 Skills"}
|
||||
placeholder={running ? "运行中可直接插话,会在它下一步生效" : "输入操作指导;用 @ 引用画布节点,用 / 或 、 引用 Skills"}
|
||||
theme={theme}
|
||||
onPromptChange={setPrompt}
|
||||
onSubmit={() => void submit()}
|
||||
|
||||
@@ -373,11 +373,19 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 面板宽度包含内边距;默认 448px 面板的输入区已不足以同时容纳控制区和快捷键提示。 */
|
||||
/* 面板宽度包含内边距;默认 448px 面板放不下完整快捷键说明,窄态降级为短文案而不是隐藏提示本身。 */
|
||||
.agent-composer-send-hint .agent-composer-send-hint-compact {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@container (max-width: 520px) {
|
||||
.agent-composer-send-hint {
|
||||
.agent-composer-send-hint .agent-composer-send-hint-full {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.agent-composer-send-hint .agent-composer-send-hint-compact {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.agent-tool-status {
|
||||
|
||||
@@ -42,7 +42,7 @@ type Props = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "onChange" | "val
|
||||
containerClassName?: string;
|
||||
highlightLabels?: boolean;
|
||||
mentionMenuWidth?: number;
|
||||
sendOnEnter?: boolean;
|
||||
sendOnEnter?: boolean | "both";
|
||||
onContentSizeChange?: (height: number) => void;
|
||||
includeAssetLibrary?: boolean;
|
||||
activeDropReferenceId?: string | null;
|
||||
@@ -50,6 +50,15 @@ type Props = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "onChange" | "val
|
||||
autoLinkEnabled?: boolean;
|
||||
};
|
||||
|
||||
// 回车提交语义由调用方决定:false 只在 ⌘/Ctrl+Enter 提交,"both" 两种都提交;Shift+Enter 始终换行。
|
||||
function shouldSubmitOnEnter(event: { key: string; ctrlKey: boolean; metaKey: boolean; shiftKey: boolean }, sendOnEnter: boolean | "both") {
|
||||
if (event.key !== "Enter" || event.shiftKey) return false;
|
||||
const modifier = event.ctrlKey || event.metaKey;
|
||||
if (sendOnEnter === false) return modifier;
|
||||
if (sendOnEnter === "both") return true;
|
||||
return !modifier;
|
||||
}
|
||||
|
||||
export const CanvasResourceMentionTextarea = forwardRef<HTMLTextAreaElement, Props>(function CanvasResourceMentionTextarea(
|
||||
{ value, references, onSelectReference, onChange, onSubmit, onKeyDown, className, containerClassName, style, highlightLabels = true, mentionMenuWidth = 320, sendOnEnter = true, onContentSizeChange, includeAssetLibrary = false, activeDropReferenceId, onReferenceFilesDrop, autoLinkEnabled = false, ...props },
|
||||
forwardedRef,
|
||||
@@ -433,7 +442,7 @@ if (event.key === "Enter" && (event.nativeEvent.isComposing || composingRef.curr
|
||||
}
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
const shouldSubmit = sendOnEnter ? !event.ctrlKey && !event.metaKey && !event.shiftKey : (event.ctrlKey || event.metaKey) && !event.shiftKey;
|
||||
const shouldSubmit = shouldSubmitOnEnter(event, sendOnEnter);
|
||||
if (onSubmit && shouldSubmit) {
|
||||
onSubmit();
|
||||
return;
|
||||
@@ -547,7 +556,7 @@ if (event.key === "Enter" && (event.nativeEvent.isComposing || composingRef.curr
|
||||
return;
|
||||
}
|
||||
}
|
||||
const shouldSubmit = event.key === "Enter" && (sendOnEnter ? !event.ctrlKey && !event.metaKey && !event.shiftKey : (event.ctrlKey || event.metaKey) && !event.shiftKey);
|
||||
const shouldSubmit = shouldSubmitOnEnter(event, sendOnEnter);
|
||||
if (shouldSubmit && onSubmit) {
|
||||
event.preventDefault();
|
||||
onSubmit();
|
||||
|
||||
@@ -121,6 +121,8 @@ describe("canvas resource mention editor", () => {
|
||||
expect(component).toContain("const token = `@[skill:${skill.skillId}] `");
|
||||
expect(component).toContain("slash.start + 1 + slash.query.length");
|
||||
expect(component).toContain("buildSkillMentionReferences(availableSlashSkills)");
|
||||
expect(component).toContain("[/、]([^\\s/、]*)$");
|
||||
expect(source("../src/components/canvas/canvas-cloud-agent-panel.tsx")).toContain("用 / 或 、 引用 Skills");
|
||||
});
|
||||
|
||||
test("skill chips use one colored icon instead of exposing the serialized token", () => {
|
||||
@@ -132,9 +134,11 @@ describe("canvas resource mention editor", () => {
|
||||
expect(component).toContain('prefix.textContent = reference.kind === "skill" ? "✦" : "@"');
|
||||
expect(component).toContain('if (reference.kind !== "skill") chip.appendChild(createInlinePreview(reference));');
|
||||
expect(component).toContain('chip.style.setProperty("--canvas-skill-mention-color", skillMentionColor(reference))');
|
||||
expect(chat).toContain("sendOnEnter={false}");
|
||||
expect(chat).toContain('sendOnEnter={canSubmit ? "both" : false}');
|
||||
expect(chat).toContain("agent-composer-resize-handle");
|
||||
expect(chat).toContain("Enter 换行 · ⌘/Ctrl+Enter 发送");
|
||||
expect(chat).toContain("Enter 发送 · Shift+Enter 换行");
|
||||
expect(css).toContain(".agent-composer-send-hint-full");
|
||||
expect(css).toContain(".agent-composer-send-hint-compact");
|
||||
expect(css).toContain(".agent-composer-prompt-scroll");
|
||||
expect(css).not.toContain(".agent-tool-row:hover");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user