mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-31 23:47:51 +00:00
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:
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>
|
||||
|
||||
Reference in new issue
Block a user