mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 13:29:44 +02:00
Compare commits
1 Commits
fix/issue-
...
agent/n-y/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2c54ae3c2 |
@@ -1,7 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { ArrowUp, Loader2, Square } from "lucide-react";
|
||||
import { Button } from "@multica/ui/components/ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@multica/ui/components/ui/tooltip";
|
||||
|
||||
interface SubmitButtonProps {
|
||||
onClick: () => void;
|
||||
@@ -9,26 +15,53 @@ interface SubmitButtonProps {
|
||||
loading?: boolean;
|
||||
running?: boolean;
|
||||
onStop?: () => void;
|
||||
/**
|
||||
* Tooltip shown over the send button when idle. Pass a string or a node
|
||||
* (e.g. `Send · ⌘↵`). Omit to render no tooltip.
|
||||
* Callers compose the shortcut hint themselves to keep this component
|
||||
* free of `@multica/core` (platform-detection) and i18n imports.
|
||||
*/
|
||||
tooltip?: ReactNode;
|
||||
/** Tooltip shown over the stop button while a run is in progress. */
|
||||
stopTooltip?: ReactNode;
|
||||
}
|
||||
|
||||
function SubmitButton({ onClick, disabled, loading, running, onStop }: SubmitButtonProps) {
|
||||
function SubmitButton({
|
||||
onClick,
|
||||
disabled,
|
||||
loading,
|
||||
running,
|
||||
onStop,
|
||||
tooltip,
|
||||
stopTooltip,
|
||||
}: SubmitButtonProps) {
|
||||
if (running) {
|
||||
return (
|
||||
const stopButton = (
|
||||
<Button size="icon-sm" onClick={onStop}>
|
||||
<Square className="fill-current" />
|
||||
</Button>
|
||||
);
|
||||
if (!stopTooltip) return stopButton;
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={stopButton} />
|
||||
<TooltipContent side="top">{stopTooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
const submitButton = (
|
||||
<Button size="icon-sm" disabled={disabled || loading} onClick={onClick}>
|
||||
{loading ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
<ArrowUp />
|
||||
)}
|
||||
{loading ? <Loader2 className="animate-spin" /> : <ArrowUp />}
|
||||
</Button>
|
||||
);
|
||||
if (!tooltip) return submitButton;
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={submitButton} />
|
||||
<TooltipContent side="top">{tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export { SubmitButton, type SubmitButtonProps };
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ContentEditor, type ContentEditorRef } from "../../editor";
|
||||
import { SubmitButton } from "@multica/ui/components/common/submit-button";
|
||||
import { useChatStore, DRAFT_NEW_SESSION } from "@multica/core/chat";
|
||||
import { createLogger } from "@multica/core/logger";
|
||||
import { enterKey, formatShortcut, modKey } from "@multica/core/platform";
|
||||
import { useT } from "../../i18n";
|
||||
|
||||
const logger = createLogger("chat.ui");
|
||||
@@ -140,8 +141,10 @@ export function ChatInput({
|
||||
// Chat is short-form — the floating formatting toolbar is
|
||||
// more distraction than feature here.
|
||||
showBubbleMenu={false}
|
||||
// Enter sends; Shift-Enter inserts a hard break.
|
||||
submitOnEnter
|
||||
// Mod+Enter submits. Bare Enter falls through to Tiptap's
|
||||
// default, which continues lists/quotes and breaks paragraphs.
|
||||
// Without this, Enter-as-send would steal the only key that
|
||||
// continues a bullet list, leaving users stuck after one item.
|
||||
/>
|
||||
</div>
|
||||
{leftAdornment && (
|
||||
@@ -156,6 +159,8 @@ export function ChatInput({
|
||||
disabled={isEmpty || !!disabled || !!noAgent}
|
||||
running={isRunning}
|
||||
onStop={onStop}
|
||||
tooltip={`${t(($) => $.input.send_tooltip)} · ${formatShortcut(modKey, enterKey)}`}
|
||||
stopTooltip={t(($) => $.input.stop_tooltip)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
90
packages/views/editor/extensions/submit-shortcut.test.ts
Normal file
90
packages/views/editor/extensions/submit-shortcut.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { getExtensionField } from "@tiptap/core";
|
||||
import type { Editor } from "@tiptap/core";
|
||||
import { createSubmitExtension } from "./submit-shortcut";
|
||||
|
||||
function getShortcuts(
|
||||
ext: ReturnType<typeof createSubmitExtension>,
|
||||
editor: Partial<Editor>,
|
||||
): Record<string, () => boolean> {
|
||||
const fn = getExtensionField<
|
||||
() => Record<string, () => boolean>
|
||||
>(ext, "addKeyboardShortcuts", {
|
||||
name: "submitShortcut",
|
||||
options: {},
|
||||
storage: {},
|
||||
editor: editor as Editor,
|
||||
type: null,
|
||||
});
|
||||
return fn?.() ?? {};
|
||||
}
|
||||
|
||||
describe("createSubmitExtension", () => {
|
||||
const baseEditor = {
|
||||
view: { composing: false } as unknown as Editor["view"],
|
||||
isActive: () => false,
|
||||
} as Partial<Editor>;
|
||||
|
||||
it("Mod-Enter always submits", () => {
|
||||
const onSubmit = vi.fn(() => true);
|
||||
const shortcuts = getShortcuts(
|
||||
createSubmitExtension(onSubmit, { submitOnEnter: false }),
|
||||
baseEditor,
|
||||
);
|
||||
|
||||
expect(shortcuts["Mod-Enter"]).toBeDefined();
|
||||
shortcuts["Mod-Enter"]!();
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("bare Enter is not bound when submitOnEnter is false", () => {
|
||||
const onSubmit = vi.fn(() => true);
|
||||
const shortcuts = getShortcuts(
|
||||
createSubmitExtension(onSubmit, { submitOnEnter: false }),
|
||||
baseEditor,
|
||||
);
|
||||
|
||||
expect(shortcuts.Enter).toBeUndefined();
|
||||
expect(onSubmit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("bare Enter submits when submitOnEnter is true", () => {
|
||||
const onSubmit = vi.fn(() => true);
|
||||
const shortcuts = getShortcuts(
|
||||
createSubmitExtension(onSubmit, { submitOnEnter: true }),
|
||||
baseEditor,
|
||||
);
|
||||
|
||||
expect(shortcuts.Enter).toBeDefined();
|
||||
expect(shortcuts.Enter!()).toBe(true);
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("Enter is suppressed during IME composition", () => {
|
||||
const onSubmit = vi.fn(() => true);
|
||||
const shortcuts = getShortcuts(
|
||||
createSubmitExtension(onSubmit, { submitOnEnter: true }),
|
||||
{
|
||||
view: { composing: true } as unknown as Editor["view"],
|
||||
isActive: () => false,
|
||||
},
|
||||
);
|
||||
|
||||
expect(shortcuts.Enter!()).toBe(false);
|
||||
expect(onSubmit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("Enter is suppressed inside a code block", () => {
|
||||
const onSubmit = vi.fn(() => true);
|
||||
const shortcuts = getShortcuts(
|
||||
createSubmitExtension(onSubmit, { submitOnEnter: true }),
|
||||
{
|
||||
view: { composing: false } as unknown as Editor["view"],
|
||||
isActive: (name: string) => name === "codeBlock",
|
||||
},
|
||||
);
|
||||
|
||||
expect(shortcuts.Enter!()).toBe(false);
|
||||
expect(onSubmit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useState, useCallback } from "react";
|
||||
import { ArrowUp, Loader2, Maximize2, Minimize2 } from "lucide-react";
|
||||
import { Button } from "@multica/ui/components/ui/button";
|
||||
import { Maximize2, Minimize2 } from "lucide-react";
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from "@multica/ui/components/ui/tooltip";
|
||||
import { cn } from "@multica/ui/lib/utils";
|
||||
import { ContentEditor, type ContentEditorRef, useFileDropZone, FileDropOverlay } from "../../editor";
|
||||
import { FileUploadButton } from "@multica/ui/components/common/file-upload-button";
|
||||
import { SubmitButton } from "@multica/ui/components/common/submit-button";
|
||||
import { useFileUpload } from "@multica/core/hooks/use-file-upload";
|
||||
import { api } from "@multica/core/api";
|
||||
import { enterKey, formatShortcut, modKey } from "@multica/core/platform";
|
||||
import { useT } from "../../i18n";
|
||||
|
||||
interface CommentInputProps {
|
||||
@@ -96,17 +97,12 @@ function CommentInput({ issueId, onSubmit }: CommentInputProps) {
|
||||
size="sm"
|
||||
onSelect={(file) => editorRef.current?.uploadFile(file)}
|
||||
/>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
disabled={isEmpty || submitting}
|
||||
<SubmitButton
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{submitting ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
<ArrowUp />
|
||||
)}
|
||||
</Button>
|
||||
disabled={isEmpty}
|
||||
loading={submitting}
|
||||
tooltip={`${t(($) => $.comment.send_tooltip)} · ${formatShortcut(modKey, enterKey)}`}
|
||||
/>
|
||||
</div>
|
||||
{isDragOver && <FileDropOverlay />}
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
"placeholder_no_agent": "Create an agent to start chatting",
|
||||
"placeholder_archived": "This session is archived",
|
||||
"placeholder_named": "Tell {{name}} what to do…",
|
||||
"placeholder_default": "Tell me what to do…"
|
||||
"placeholder_default": "Tell me what to do…",
|
||||
"send_tooltip": "Send",
|
||||
"stop_tooltip": "Stop"
|
||||
},
|
||||
"message_list": {
|
||||
"show_details": "Show details",
|
||||
|
||||
@@ -173,6 +173,7 @@
|
||||
"reply_count_one": "{{count}} reply",
|
||||
"reply_count_other": "{{count}} replies",
|
||||
"leave_comment_placeholder": "Leave a comment...",
|
||||
"send_tooltip": "Send",
|
||||
"expand_tooltip": "Expand",
|
||||
"collapse_tooltip": "Collapse",
|
||||
"resolve": {
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
"placeholder_no_agent": "创建一个智能体后才能开始对话",
|
||||
"placeholder_archived": "此会话已归档",
|
||||
"placeholder_named": "告诉 {{name}} 该做什么...",
|
||||
"placeholder_default": "告诉我该做什么..."
|
||||
"placeholder_default": "告诉我该做什么...",
|
||||
"send_tooltip": "发送",
|
||||
"stop_tooltip": "停止"
|
||||
},
|
||||
"message_list": {
|
||||
"show_details": "查看详情",
|
||||
|
||||
@@ -171,6 +171,7 @@
|
||||
"delete_failed": "删除评论失败",
|
||||
"reply_count_other": "{{count}} 条回复",
|
||||
"leave_comment_placeholder": "留下评论...",
|
||||
"send_tooltip": "发送",
|
||||
"expand_tooltip": "展开",
|
||||
"collapse_tooltip": "收起",
|
||||
"resolve": {
|
||||
|
||||
Reference in New Issue
Block a user