promptbox: default both Enter and Shift+Enter to newline (#1454)

Only the submit button triggers generation by default. The "Enter to
generate" toggle (available in both app and website settings top right
nav) still allows opting into Enter-to-submit.
This commit is contained in:
BFlat authored and GitHub committed 2026-04-30 10:02:10 +07:00
1 parent e1b8864a8a
commit 8f9607cc5d
14 files changed
+132 -31

No files matched your search

@@ -21,9 +21,11 @@ import {
faBars,
faXmark,
faGem,
faCog,
} from "@fortawesome/pro-solid-svg-icons";
import { TaskQueue } from "./task-queue";
import { CreditsModal } from "../credits-modal";
import { SettingsModal } from "../settings-modal/SettingsModal";
const NAV_ITEMS = [
{ name: "Home", href: "/" },
@@ -80,6 +82,7 @@ export default function Navbar() {
// null = unknown (still loading) so we don't flash the upgrade button
const [hasPaidPlan, setHasPaidPlan] = useState<boolean | null>(null);
const [creditsModalOpen, setCreditsModalOpen] = useState(false);
const [settingsOpen, setSettingsOpen] = useState(false);
// Check session on mount and when auth changes or location changes
useEffect(() => {
@@ -300,6 +303,14 @@ export default function Navbar() {
<TaskQueue />
<Button
variant="secondary"
onClick={() => setSettingsOpen(true)}
className="h-[38px] w-[38px]"
>
<FontAwesomeIcon icon={faCog} className="text-base" />
</Button>
<Menu as="div" className="relative ml-3">
<div>
<MenuButton className="flex rounded-full bg-gray-800 text-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800">
@@ -355,6 +366,7 @@ export default function Navbar() {
>
Pricing
</Link>
<div className="flex gap-2">
<Link to="/login">
<Button
@@ -431,6 +443,16 @@ export default function Navbar() {
<div className="my-3 border-t border-white/[0.06]" />
<button
onClick={() => setSettingsOpen(true)}
className="flex w-full items-center gap-2 rounded-md px-3 py-[7px] text-[13px] font-medium text-white/60 active:bg-white/5 transition-colors"
>
<FontAwesomeIcon icon={faCog} className="text-xs" />
Settings
</button>
<div className="my-3 border-t border-white/[0.06]" />
{!isLoading && user ? (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-1.5">
@@ -506,6 +528,11 @@ export default function Navbar() {
onClose={() => setCreditsModalOpen(false)}
/>
<SettingsModal
isOpen={settingsOpen}
onClose={() => setSettingsOpen(false)}
/>
<style>{`
.link-underline {
transform-origin: left center;
@@ -591,10 +591,7 @@ export const MentionTextarea = forwardRef<HTMLDivElement, MentionTextareaProps>(
}
}
// Default: plain Enter inserts a newline (instead of letting the
// contentEditable create a <div>). Shift/Cmd+Enter falls through to
// externalOnKeyDown so the host can submit.
if (e.key === "Enter" && !e.shiftKey && !e.metaKey) {
if (e.key === "Enter") {
e.preventDefault();
document.execCommand("insertLineBreak");
handleInput();
@@ -21,6 +21,7 @@ import { Tooltip } from "@storyteller/ui-tooltip";
import { ImagePromptRow } from "./ImagePromptRow";
import { MentionTextarea } from "./MentionTextarea";
import type { RefImage, MentionItem } from "./types";
import { useEnterToGenerateStore } from "../../lib/enter-to-generate-store";
// ── @-mention color palette ─────────────────────────────────────────────
@@ -144,6 +145,7 @@ export const PromptBox = forwardRef<HTMLDivElement, PromptBoxProps>(
const textareaRef = useRef<HTMLTextAreaElement>(null);
const highlightRef = useRef<HTMLDivElement>(null);
const mentionEditorRef = useRef<HTMLDivElement>(null);
const enterToGenerate = useEnterToGenerateStore((s) => s.enabled);
const [isFocused, setIsFocused] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const [showImagePrompts, setShowImagePrompts] = useState(false);
@@ -291,8 +293,7 @@ export const PromptBox = forwardRef<HTMLDivElement, PromptBoxProps>(
}
}
// Default: Enter inserts a newline; Shift+Enter submits.
if (e.key === "Enter" && e.shiftKey) {
if (e.key === "Enter" && enterToGenerate && !e.shiftKey) {
e.preventDefault();
onSubmit();
}
@@ -436,8 +437,7 @@ export const PromptBox = forwardRef<HTMLDivElement, PromptBoxProps>(
)}
colorMap={mentionColorMap}
onKeyDown={(e) => {
// Default: Enter inserts a newline; Shift+Enter (or Cmd+Enter) submits.
if (e.key === "Enter" && (e.shiftKey || e.metaKey)) {
if (e.key === "Enter" && enterToGenerate && !e.shiftKey && !e.metaKey) {
e.preventDefault();
onSubmit();
}
@@ -0,0 +1,59 @@
import { Modal } from "@storyteller/ui-modal";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCog } from "@fortawesome/pro-solid-svg-icons";
import { Switch } from "@storyteller/ui-switch";
import { useEnterToGenerateStore } from "../../lib/enter-to-generate-store";
interface SettingsModalProps {
isOpen: boolean;
onClose: () => void;
}
export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
const enterToGenerate = useEnterToGenerateStore((s) => s.enabled);
const setEnterToGenerate = useEnterToGenerateStore((s) => s.setEnabled);
return (
<Modal isOpen={isOpen} onClose={onClose} className="max-w-3xl" childPadding={false}>
<div className="h-[560px]">
<div className="grid h-full grid-cols-12 gap-3">
<div className="relative col-span-4 p-3 pt-2 after:absolute after:right-0 after:top-0 after:h-full after:w-px after:bg-ui-panel-border">
<div className="flex items-center gap-2.5 py-0.5">
<h2 className="text-[18px] font-semibold opacity-80">Settings</h2>
</div>
<hr className="my-2 w-full border-ui-panel-border" />
<div className="space-y-1">
<button className="h-9 w-full rounded-lg p-2 text-left bg-[#63636B]/20">
<div className="flex items-center gap-2.5 text-sm">
<FontAwesomeIcon icon={faCog} />
General
</div>
</button>
</div>
</div>
<div className="col-span-8 flex h-full flex-col overflow-y-auto relative">
<div className="w-full border-b border-ui-panel-border py-2.5 ps-0">
<h2 className="text-[18px] font-semibold">General</h2>
</div>
<div className="p-3 ps-0 text-sm h-full">
<div className="space-y-4 text-base-fg">
<div className="flex flex-col gap-2 pt-3">
<div className="flex flex-col gap-0.5">
<p className="text-sm font-medium">Enter to generate</p>
<p className="text-xs opacity-70">
When on, pressing Enter submits the prompt and Shift+Enter
adds a new line. When off (default), both Enter and
Shift+Enter add a new line — use the button to submit.
</p>
</div>
<Switch enabled={enterToGenerate} setEnabled={setEnterToGenerate} offClassName="bg-white/20" />
</div>
</div>
</div>
</div>
</div>
</div>
</Modal>
);
}
@@ -0,0 +1,17 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
interface EnterToGenerateStore {
enabled: boolean;
setEnabled: (enabled: boolean) => void;
}
export const useEnterToGenerateStore = create<EnterToGenerateStore>()(
persist(
(set) => ({
enabled: false,
setEnabled: (enabled) => set({ enabled }),
}),
{ name: "artcraft-website-enter-to-generate" },
),
);
@@ -625,13 +625,11 @@ export const MentionTextarea = forwardRef<HTMLDivElement, MentionTextareaProps>(
}
// Insert a newline instead of letting the contentEditable create a <div>.
// When `enterToGenerate` is off (default), plain Enter inserts a newline
// and Shift/Cmd+Enter submits via externalOnKeyDown. When the user opts
// into Enter-to-generate, the modifier combo is what inserts the newline.
// When `enterToGenerate` is off (default), all Enter combos insert a newline
// (only the button submits). When the user opts into Enter-to-generate,
// Shift/Cmd+Enter inserts a newline and plain Enter submits.
if (e.key === "Enter") {
const newlineCombo = enterToGenerate
? e.shiftKey || e.metaKey
: !e.shiftKey && !e.metaKey;
const newlineCombo = !enterToGenerate || e.shiftKey || e.metaKey;
if (newlineCombo) {
e.preventDefault();
document.execCommand("insertLineBreak");
@@ -332,7 +332,7 @@ export const PromptBox2D = ({
e.stopPropagation();
if (e.key !== "Enter") return;
const isSubmitCombo = enterToGenerate ? !e.shiftKey : e.shiftKey;
const isSubmitCombo = enterToGenerate && !e.shiftKey;
if (isSubmitCombo) {
e.preventDefault();
handleGenerate();
@@ -461,7 +461,7 @@ export const PromptBox3D = ({
e.stopPropagation();
if (e.key !== "Enter") return;
const isSubmitCombo = enterToGenerate ? !e.shiftKey : e.shiftKey;
const isSubmitCombo = enterToGenerate && !e.shiftKey;
if (isSubmitCombo) {
e.preventDefault();
handleEnqueue();
@@ -239,7 +239,7 @@ export const PromptBoxEdit = ({
e.stopPropagation();
if (e.key !== "Enter") return;
const isSubmitCombo = enterToGenerate ? !e.shiftKey : e.shiftKey;
const isSubmitCombo = enterToGenerate && !e.shiftKey;
if (isSubmitCombo) {
e.preventDefault();
const busy = Boolean(isEnqueueing ?? internalEnqueueing);
@@ -397,7 +397,7 @@ export const PromptBoxImage = ({
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key !== "Enter") return;
const isSubmitCombo = enterToGenerate ? !e.shiftKey : e.shiftKey;
const isSubmitCombo = enterToGenerate && !e.shiftKey;
if (isSubmitCombo) {
e.preventDefault();
handleEnqueue();
@@ -865,7 +865,7 @@ export const PromptBoxVideo = ({
}
if (e.key !== "Enter") return;
const isSubmitCombo = enterToGenerate ? !e.shiftKey : e.shiftKey;
const isSubmitCombo = enterToGenerate && !e.shiftKey;
if (isSubmitCombo) {
e.preventDefault();
@@ -991,9 +991,7 @@ export const PromptBoxVideo = ({
className="promptbox-scrollbar text-md relative mb-2 min-h-[2.5em] w-full resize-y overflow-y-auto rounded bg-transparent pb-2 pr-2 pt-1 text-base-fg"
onKeyDown={(e) => {
if (e.key !== "Enter") return;
const isSubmitCombo = enterToGenerate
? !e.shiftKey
: e.shiftKey;
const isSubmitCombo = enterToGenerate && !e.shiftKey;
if (isSubmitCombo) {
e.preventDefault();
if (
@@ -209,7 +209,7 @@ export const usePromptEditStore = create<PromptEditStore>()((set) => ({
// ----- Enter-to-Generate Preference Store -----
// Controls how the Enter key behaves inside prompt boxes.
// false (default): Enter inserts a newline; Shift+Enter submits.
// false (default): Both Enter and Shift+Enter insert a newline; only the button submits.
// true: Enter submits; Shift+Enter inserts a newline.
// Persisted to localStorage so the choice survives reloads.
const ENTER_TO_GENERATE_STORAGE_KEY = "artcraft_enter_to_generate";
@@ -12,10 +12,14 @@ import { Label } from "@storyteller/ui-label";
import { Switch } from "@storyteller/ui-switch";
import { DownloadDirectoryReveal } from "@storyteller/tauri-api";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faFolder, faMagnifyingGlass, faRotateLeft } from "@fortawesome/pro-solid-svg-icons";
import {
faFolder,
faMagnifyingGlass,
faRotateLeft,
} from "@fortawesome/pro-solid-svg-icons";
import { useEnterToGenerateStore } from "@storyteller/ui-promptbox";
interface MiscSettingsPaneProps { }
interface MiscSettingsPaneProps {}
export const MiscSettingsPane = (args: MiscSettingsPaneProps) => {
const [preferences, setPreferences] = useState<
@@ -114,8 +118,8 @@ export const MiscSettingsPane = (args: MiscSettingsPaneProps) => {
<Label htmlFor="enter-to-generate">Enter to generate</Label>
<p className="text-xs opacity-70">
When on, pressing Enter submits the prompt and Shift+Enter adds a
new line. When off (default), Enter adds a new line and Shift+Enter
submits.
new line. When off (default), both Enter and Shift+Enter add a new
line - use only the button to submit.
</p>
</div>
<Switch enabled={enterToGenerate} setEnabled={setEnterToGenerate} />
@@ -6,25 +6,26 @@ interface SwitchProps {
enabled: boolean;
setEnabled: (enabled: boolean) => void;
className?: string;
offClassName?: string;
}
export function Switch({ enabled, setEnabled, className }: SwitchProps) {
export function Switch({ enabled, setEnabled, className, offClassName }: SwitchProps) {
return (
<HeadlessSwitch checked={enabled} onChange={setEnabled} as={Fragment}>
{({ checked, disabled }) => (
<button
className={clsx(
"group inline-flex h-6 w-11 items-center rounded-full",
checked ? "bg-primary" : "bg-action",
checked ? "bg-primary" : (offClassName ?? "bg-action"),
disabled && "cursor-not-allowed opacity-50",
className
className,
)}
>
<span className="sr-only">Enable notifications</span>
<span
className={clsx(
"size-4 rounded-full bg-white transition",
checked ? "translate-x-6" : "translate-x-1"
checked ? "translate-x-6" : "translate-x-1",
)}
/>
</button>