mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-31 15:55:54 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39c3827e5f | ||
|
|
4a20522eec | ||
|
|
25cf131473 |
@@ -19,6 +19,9 @@ interface MentionTextareaProps {
|
||||
mentionItems: MentionItem[];
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
/** Inline style applied to the editable element (e.g. a maxHeight cap or
|
||||
* resize override for the fullscreen modal). */
|
||||
style?: React.CSSProperties;
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
@@ -294,6 +297,7 @@ export const MentionTextarea = forwardRef<HTMLDivElement, MentionTextareaProps>(
|
||||
mentionItems,
|
||||
placeholder,
|
||||
className,
|
||||
style,
|
||||
onKeyDown: externalOnKeyDown,
|
||||
onFocus,
|
||||
onBlur,
|
||||
@@ -665,6 +669,7 @@ export const MentionTextarea = forwardRef<HTMLDivElement, MentionTextareaProps>(
|
||||
onPaste={handlePaste}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
style={style}
|
||||
className={twMerge(
|
||||
className,
|
||||
"outline-none whitespace-pre-wrap break-words overflow-y-auto resize-y",
|
||||
|
||||
@@ -323,24 +323,31 @@ export const PromptBox = forwardRef<HTMLDivElement, PromptBoxProps>(
|
||||
});
|
||||
}, [prompt, hasMentionItems, mentionRegex, mentionLabelMap, mentionItems]);
|
||||
|
||||
// Shared reference-image row. Inline it attaches to the top of the box
|
||||
// (square bottom); the fullscreen modal passes a className to round the
|
||||
// bottom corners since it sits in-flow above the controls there.
|
||||
const renderImagePromptRow = (className?: string) =>
|
||||
supportsImagePrompts ? (
|
||||
<ImagePromptRow
|
||||
maxImagePromptCount={maxImagePromptCount}
|
||||
referenceImages={referenceImages}
|
||||
setReferenceImages={onReferenceImagesChange}
|
||||
onPickFromLibrary={onPickFromLibrary}
|
||||
onClearAll={onClearAllRefs}
|
||||
isVideo={isVideo}
|
||||
isReferenceMode={isReferenceMode}
|
||||
endFrameImage={endFrameImage}
|
||||
setEndFrameImage={onEndFrameImageChange}
|
||||
showEndFrameSection={showEndFrameSection}
|
||||
onPickEndFrameFromLibrary={onPickEndFrameFromLibrary}
|
||||
className={className}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div ref={ref} className="prompt-box-root">
|
||||
<div className="relative flex flex-col">
|
||||
{isImageRowVisible && (
|
||||
<ImagePromptRow
|
||||
maxImagePromptCount={maxImagePromptCount}
|
||||
referenceImages={referenceImages}
|
||||
setReferenceImages={onReferenceImagesChange}
|
||||
onPickFromLibrary={onPickFromLibrary}
|
||||
onClearAll={onClearAllRefs}
|
||||
isVideo={isVideo}
|
||||
isReferenceMode={isReferenceMode}
|
||||
endFrameImage={endFrameImage}
|
||||
setEndFrameImage={onEndFrameImageChange}
|
||||
showEndFrameSection={showEndFrameSection}
|
||||
onPickEndFrameFromLibrary={onPickEndFrameFromLibrary}
|
||||
/>
|
||||
)}
|
||||
{isImageRowVisible && renderImagePromptRow()}
|
||||
|
||||
{mediaReferenceRow}
|
||||
|
||||
@@ -555,14 +562,25 @@ export const PromptBox = forwardRef<HTMLDivElement, PromptBoxProps>(
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PromptFullscreenModal isOpen={isFullscreen} onClose={closeFullscreen}>
|
||||
<PromptFullscreenModal
|
||||
isOpen={isFullscreen}
|
||||
onClose={closeFullscreen}
|
||||
footerControls={
|
||||
<>
|
||||
{modelSelector}
|
||||
{leftToolbar}
|
||||
</>
|
||||
}
|
||||
imagePromptRow={renderImagePromptRow("rounded-2xl sm:rounded-b-2xl")}
|
||||
>
|
||||
{hasMentionItems && mentionItems ? (
|
||||
<MentionTextarea
|
||||
value={prompt}
|
||||
onChange={onPromptChange}
|
||||
mentionItems={mentionItems}
|
||||
placeholder={placeholder}
|
||||
className="promptbox-scrollbar h-full w-full text-base-fg placeholder-base-fg/60"
|
||||
className="promptbox-scrollbar h-full min-h-0 w-full overflow-y-auto text-base-fg placeholder-base-fg/60"
|
||||
style={{ resize: "none" }}
|
||||
colorMap={mentionColorMap}
|
||||
onKeyDown={(e) => {
|
||||
if (
|
||||
@@ -579,7 +597,7 @@ export const PromptBox = forwardRef<HTMLDivElement, PromptBoxProps>(
|
||||
) : (
|
||||
<textarea
|
||||
placeholder={placeholder}
|
||||
className="promptbox-scrollbar text-md h-full w-full resize-none bg-transparent text-base-fg placeholder-base-fg/60 focus:outline-none"
|
||||
className="promptbox-scrollbar text-md h-full min-h-0 w-full resize-none overflow-y-auto bg-transparent text-base-fg placeholder-base-fg/60 focus:outline-none"
|
||||
value={prompt}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { Tooltip } from "@storyteller/ui-tooltip";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faUpRightAndDownLeftFromCenter } from "@fortawesome/pro-solid-svg-icons";
|
||||
@@ -56,12 +57,19 @@ interface PromptFullscreenModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
/** Controls rendered in the footer, left of the Done button (e.g. the model
|
||||
* selector + character button). */
|
||||
footerControls?: ReactNode;
|
||||
/** Reference-image row, always shown in focus mode. */
|
||||
imagePromptRow?: ReactNode;
|
||||
}
|
||||
|
||||
export const PromptFullscreenModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
children,
|
||||
footerControls,
|
||||
imagePromptRow,
|
||||
}: PromptFullscreenModalProps) => {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -79,18 +87,30 @@ export const PromptFullscreenModal = ({
|
||||
accessibleTitle="Prompt focus mode"
|
||||
closeOnOutsideClick
|
||||
closeOnEsc
|
||||
className="h-[70vh] w-full max-w-4xl"
|
||||
className="w-full max-w-4xl"
|
||||
backdropClassName="backdrop-blur-md"
|
||||
>
|
||||
{/* Title lives inside the flex column (not the Modal title slot) so the
|
||||
editor fits the fixed panel height cleanly. No overflow-hidden here —
|
||||
the mention autocomplete dropdown renders outside the editor bounds
|
||||
and must not be clipped. */}
|
||||
<div ref={contentRef} className="flex h-full flex-col gap-2">
|
||||
<h2 className="text-lg font-bold text-base-fg">Prompt</h2>
|
||||
{/* flex column so a flex-1 editor (the MentionTextarea root wrapper)
|
||||
or an h-full textarea fills the remaining space. */}
|
||||
<div className="flex min-h-0 flex-1 flex-col">{children}</div>
|
||||
{/* Explicit inline height (not a Tailwind arbitrary class) so the column
|
||||
is reliably bounded across the modal's nested wrappers — that's what
|
||||
lets the editor scroll instead of stretching the panel. min-h-0 lets
|
||||
the flex children shrink below content size. */}
|
||||
<div className="flex min-h-0 flex-col gap-2" style={{ height: "70vh" }}>
|
||||
<h2 className="shrink-0 text-lg font-bold text-base-fg">Prompt</h2>
|
||||
{/* flex-1 so the editor takes only the space left after the (shrink-0)
|
||||
image row + footer — keeps it from overflowing when the window is
|
||||
shorter. min-h-0 + overflow-hidden bound the editor area so its own
|
||||
textarea scrolls instead of pushing the layout. */}
|
||||
<div
|
||||
ref={contentRef}
|
||||
className="flex min-h-0 flex-1 flex-col overflow-hidden"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{imagePromptRow && <div className="shrink-0">{imagePromptRow}</div>}
|
||||
<div className="flex shrink-0 items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">{footerControls}</div>
|
||||
<Button onClick={onClose}>Done</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -25,6 +25,8 @@ interface MentionTextareaProps {
|
||||
mentionItems: MentionItem[];
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
/** Inline style applied to the editable element (e.g. a maxHeight cap). */
|
||||
style?: React.CSSProperties;
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
@@ -312,6 +314,7 @@ export const MentionTextarea = forwardRef<HTMLDivElement, MentionTextareaProps>(
|
||||
mentionItems,
|
||||
placeholder,
|
||||
className,
|
||||
style,
|
||||
onKeyDown: externalOnKeyDown,
|
||||
onFocus,
|
||||
onBlur,
|
||||
@@ -694,6 +697,7 @@ export const MentionTextarea = forwardRef<HTMLDivElement, MentionTextareaProps>(
|
||||
onPaste={handlePaste}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
style={style}
|
||||
className={twMerge(
|
||||
className,
|
||||
"outline-none whitespace-pre-wrap [overflow-wrap:anywhere] overflow-y-auto",
|
||||
|
||||
@@ -558,10 +558,27 @@ export const PromptBox2D = ({
|
||||
onClose={closeFullscreen}
|
||||
promptLength={prompt.length}
|
||||
maxLength={maxLen}
|
||||
footerControls={modelSelector}
|
||||
imagePromptRow={
|
||||
selectedImageModel?.canUseImagePrompt ? (
|
||||
<ImagePromptRow
|
||||
visible={true}
|
||||
maxImagePromptCount={Math.max(
|
||||
1,
|
||||
selectedImageModel?.maxImagePromptCount ?? 1,
|
||||
)}
|
||||
allowUpload={true}
|
||||
referenceImages={referenceImages}
|
||||
setReferenceImages={setReferenceImages}
|
||||
uploadImage={uploadImage as any}
|
||||
className="relative top-auto rounded-2xl"
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<textarea
|
||||
placeholder="Describe your image..."
|
||||
className="promptbox-scrollbar text-md h-full w-full resize-none rounded bg-transparent text-base-fg placeholder-base-fg/60 focus:outline-none"
|
||||
className="promptbox-scrollbar text-md h-full min-h-0 w-full resize-none overflow-y-auto rounded bg-transparent text-base-fg placeholder-base-fg/60 focus:outline-none"
|
||||
value={prompt}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
|
||||
@@ -898,10 +898,27 @@ export const PromptBox3D = ({
|
||||
onClose={closeFullscreen}
|
||||
promptLength={prompt.length}
|
||||
maxLength={maxLen}
|
||||
footerControls={modelSelector}
|
||||
imagePromptRow={
|
||||
selectedImageModel?.canUseImagePrompt ? (
|
||||
<ImagePromptRow
|
||||
visible={true}
|
||||
maxImagePromptCount={Math.max(
|
||||
1,
|
||||
selectedImageModel?.maxImagePromptCount ?? 1,
|
||||
)}
|
||||
allowUpload={true}
|
||||
referenceImages={referenceImages}
|
||||
setReferenceImages={setReferenceImages}
|
||||
uploadImage={uploadImage as any}
|
||||
className="relative top-auto rounded-2xl"
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<textarea
|
||||
placeholder="Describe your image..."
|
||||
className="promptbox-scrollbar text-md h-full w-full resize-none rounded bg-transparent text-base-fg placeholder-base-fg/60 focus:outline-none"
|
||||
className="promptbox-scrollbar text-md h-full min-h-0 w-full resize-none overflow-y-auto rounded bg-transparent text-base-fg placeholder-base-fg/60 focus:outline-none"
|
||||
value={prompt}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
|
||||
@@ -568,10 +568,26 @@ export const PromptBoxEdit = ({
|
||||
onClose={closeFullscreen}
|
||||
promptLength={prompt.length}
|
||||
maxLength={maxLen}
|
||||
imagePromptRow={
|
||||
selectedImageModel?.canUseImagePrompt ? (
|
||||
<ImagePromptRow
|
||||
visible={true}
|
||||
maxImagePromptCount={Math.max(
|
||||
1,
|
||||
selectedImageModel?.maxImagePromptCount ?? 1,
|
||||
)}
|
||||
allowUpload={true}
|
||||
referenceImages={referenceImages}
|
||||
setReferenceImages={setReferenceImages}
|
||||
uploadImage={uploadImage}
|
||||
className="relative top-auto rounded-2xl"
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<textarea
|
||||
placeholder="Write what you want to change in your image and click generate..."
|
||||
className="promptbox-scrollbar text-md h-full w-full resize-none rounded bg-transparent text-base-fg placeholder-base-fg/60 focus:outline-none"
|
||||
className="promptbox-scrollbar text-md h-full min-h-0 w-full resize-none overflow-y-auto rounded bg-transparent text-base-fg placeholder-base-fg/60 focus:outline-none"
|
||||
value={prompt}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
|
||||
@@ -574,10 +574,29 @@ export const PromptBoxImage = ({
|
||||
onClose={closeFullscreen}
|
||||
promptLength={prompt.length}
|
||||
maxLength={maxLen}
|
||||
footerControls={modelSelector}
|
||||
imagePromptRow={
|
||||
selectedModel?.canUseImagePrompt ? (
|
||||
<ImagePromptRow
|
||||
visible={true}
|
||||
maxImagePromptCount={Math.max(
|
||||
1,
|
||||
selectedModel?.maxImagePromptCount ?? 1,
|
||||
)}
|
||||
allowUpload={true}
|
||||
referenceImages={referenceImages}
|
||||
setReferenceImages={setReferenceImages}
|
||||
uploadImage={uploadImage as any}
|
||||
// Reset the inline row's absolute "float above the box"
|
||||
// positioning so it sits in-flow in the modal, with rounded corners.
|
||||
className="relative top-auto rounded-2xl"
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<textarea
|
||||
placeholder="Describe what you want in the image..."
|
||||
className="promptbox-scrollbar text-md h-full w-full resize-none rounded bg-transparent text-base-fg placeholder-base-fg/60 focus:outline-none"
|
||||
className="promptbox-scrollbar text-md h-full min-h-0 w-full resize-none overflow-y-auto rounded bg-transparent text-base-fg placeholder-base-fg/60 focus:outline-none"
|
||||
value={prompt}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
|
||||
@@ -967,6 +967,72 @@ export const PromptBoxVideo = ({
|
||||
}
|
||||
}, [selectedModel, endFrameImage, setEndFrameImage]);
|
||||
|
||||
// Shared reference-image row. Inline it floats above the box (default absolute
|
||||
// positioning); the fullscreen modal passes a className to reset that so it
|
||||
// sits in-flow above the controls with rounded corners.
|
||||
const renderImagePromptRow = (className?: string) => (
|
||||
<ImagePromptRow
|
||||
visible={true}
|
||||
isVideo={true}
|
||||
isReferenceMode={isReferenceMode}
|
||||
maxImagePromptCount={maxImageCount}
|
||||
allowUpload={true}
|
||||
referenceImages={referenceImages}
|
||||
setReferenceImages={setReferenceImages}
|
||||
onImageClick={(image) => {
|
||||
setContent(
|
||||
<img
|
||||
src={image.url}
|
||||
alt="Reference preview"
|
||||
className="h-full w-full object-contain"
|
||||
/>,
|
||||
);
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
uploadImage={uploadImage}
|
||||
endFrameImage={isReferenceMode ? undefined : endFrameImage}
|
||||
setEndFrameImage={isReferenceMode ? undefined : setEndFrameImage}
|
||||
allowUploadEnd={!isReferenceMode && !!selectedModel?.endFrame}
|
||||
showEndFrameSection={!isReferenceMode && !!selectedModel?.endFrame}
|
||||
referenceVideos={isReferenceMode ? referenceVideos : undefined}
|
||||
setReferenceVideos={isReferenceMode ? setReferenceVideos : undefined}
|
||||
maxVideoCount={selectedModel?.maxReferenceVideos ?? 3}
|
||||
maxVideoRefDuration={selectedModel?.maxVideoRefDuration ?? 15}
|
||||
showVideoReferenceSection={isReferenceMode}
|
||||
uploadVideo={uploadVideo}
|
||||
referenceAudios={isReferenceMode ? referenceAudios : undefined}
|
||||
setReferenceAudios={isReferenceMode ? setReferenceAudios : undefined}
|
||||
maxAudioCount={selectedModel?.maxReferenceAudios ?? 2}
|
||||
maxAudioRefDuration={selectedModel?.maxAudioRefDuration ?? 15}
|
||||
uploadAudio={uploadAudio}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
|
||||
// Character button (seedance_2p0 only), reused in the fullscreen footer.
|
||||
const characterButtonEl =
|
||||
selectedModel?.id === "seedance_2p0" ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCharactersModalOpen(true)}
|
||||
className="flex h-9 items-center justify-center gap-1 rounded-lg border border-ui-controls-border bg-ui-controls px-3 text-sm font-medium text-base-fg transition-all duration-150 hover:bg-ui-controls/80 active:scale-95"
|
||||
>
|
||||
@Characters
|
||||
</button>
|
||||
) : null;
|
||||
|
||||
// Input-mode (keyframe/reference) picker, reused in the fullscreen footer.
|
||||
const inputModeEl = inputModeOptions ? (
|
||||
<Tooltip content="Input Mode" position="top" className="z-50" closeOnClick>
|
||||
<PopoverMenu
|
||||
items={inputModeOptions}
|
||||
onSelect={handleInputModeSelect}
|
||||
mode="toggle"
|
||||
panelTitle="Input Mode"
|
||||
/>
|
||||
</Tooltip>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
@@ -979,47 +1045,7 @@ export const PromptBoxVideo = ({
|
||||
{content}
|
||||
</Modal>
|
||||
<div className="relative z-20 flex flex-col gap-3">
|
||||
{isImageRowVisible && (
|
||||
<ImagePromptRow
|
||||
visible={true}
|
||||
isVideo={true}
|
||||
isReferenceMode={isReferenceMode}
|
||||
maxImagePromptCount={maxImageCount}
|
||||
allowUpload={true}
|
||||
referenceImages={referenceImages}
|
||||
setReferenceImages={setReferenceImages}
|
||||
onImageClick={(image) => {
|
||||
setContent(
|
||||
<img
|
||||
src={image.url}
|
||||
alt="Reference preview"
|
||||
className="h-full w-full object-contain"
|
||||
/>,
|
||||
);
|
||||
setIsModalOpen(true);
|
||||
}}
|
||||
uploadImage={uploadImage}
|
||||
endFrameImage={isReferenceMode ? undefined : endFrameImage}
|
||||
setEndFrameImage={isReferenceMode ? undefined : setEndFrameImage}
|
||||
allowUploadEnd={!isReferenceMode && !!selectedModel?.endFrame}
|
||||
showEndFrameSection={!isReferenceMode && !!selectedModel?.endFrame}
|
||||
referenceVideos={isReferenceMode ? referenceVideos : undefined}
|
||||
setReferenceVideos={
|
||||
isReferenceMode ? setReferenceVideos : undefined
|
||||
}
|
||||
maxVideoCount={selectedModel?.maxReferenceVideos ?? 3}
|
||||
maxVideoRefDuration={selectedModel?.maxVideoRefDuration ?? 15}
|
||||
showVideoReferenceSection={isReferenceMode}
|
||||
uploadVideo={uploadVideo}
|
||||
referenceAudios={isReferenceMode ? referenceAudios : undefined}
|
||||
setReferenceAudios={
|
||||
isReferenceMode ? setReferenceAudios : undefined
|
||||
}
|
||||
maxAudioCount={selectedModel?.maxReferenceAudios ?? 2}
|
||||
maxAudioRefDuration={selectedModel?.maxAudioRefDuration ?? 15}
|
||||
uploadAudio={uploadAudio}
|
||||
/>
|
||||
)}
|
||||
{isImageRowVisible && renderImagePromptRow()}
|
||||
<div
|
||||
className={twMerge(
|
||||
"glass relative w-full rounded-2xl p-4",
|
||||
@@ -1184,31 +1210,9 @@ export const PromptBoxVideo = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{inputModeOptions && (
|
||||
<Tooltip
|
||||
content="Input Mode"
|
||||
position="top"
|
||||
className="z-50"
|
||||
closeOnClick={true}
|
||||
>
|
||||
<PopoverMenu
|
||||
items={inputModeOptions}
|
||||
onSelect={handleInputModeSelect}
|
||||
mode="toggle"
|
||||
panelTitle="Input Mode"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{inputModeEl}
|
||||
|
||||
{selectedModel?.id === "seedance_2p0" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCharactersModalOpen(true)}
|
||||
className="flex h-9 items-center justify-center gap-1 rounded-lg border border-ui-controls-border bg-ui-controls px-3 text-sm font-medium text-base-fg transition-all duration-150 hover:bg-ui-controls/80 active:scale-95"
|
||||
>
|
||||
@Characters
|
||||
</button>
|
||||
)}
|
||||
{characterButtonEl}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{modelNeedsAnImageButNoneAreSelected && (
|
||||
@@ -1336,6 +1340,14 @@ export const PromptBoxVideo = ({
|
||||
onClose={closeFullscreen}
|
||||
promptLength={prompt.length}
|
||||
maxLength={maxLen}
|
||||
footerControls={
|
||||
<>
|
||||
{modelSelector}
|
||||
{inputModeEl}
|
||||
{characterButtonEl}
|
||||
</>
|
||||
}
|
||||
imagePromptRow={renderImagePromptRow("relative top-auto rounded-2xl")}
|
||||
>
|
||||
{hasAnyMentionables ? (
|
||||
<MentionTextarea
|
||||
@@ -1348,12 +1360,13 @@ export const PromptBoxVideo = ({
|
||||
? "Use @Image1, @Video1, @Audio1... to reference uploads in prompt..."
|
||||
: "Describe what you want to happen in the video..."
|
||||
}
|
||||
className="promptbox-scrollbar text-md h-full w-full resize-none overflow-y-auto rounded bg-transparent text-base-fg"
|
||||
className="promptbox-scrollbar text-md h-full min-h-0 w-full resize-none overflow-y-auto rounded bg-transparent text-base-fg"
|
||||
style={{ resize: "none" }}
|
||||
/>
|
||||
) : (
|
||||
<textarea
|
||||
placeholder="Describe what you want to happen in the video..."
|
||||
className="promptbox-scrollbar text-md h-full w-full resize-none rounded bg-transparent text-base-fg placeholder-base-fg/60 focus:outline-none"
|
||||
className="promptbox-scrollbar text-md h-full min-h-0 w-full resize-none overflow-y-auto rounded bg-transparent text-base-fg placeholder-base-fg/60 focus:outline-none"
|
||||
value={prompt}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { focusEditorAtEnd } from "./focusEditorAtEnd";
|
||||
|
||||
// Shared "focus mode" overlay for prompt editors. Reuses @storyteller/ui-modal
|
||||
@@ -20,6 +21,17 @@ interface PromptFullscreenModalProps {
|
||||
* MentionTextarea bound to the same value + onChange as the inline one.
|
||||
*/
|
||||
children: ReactNode;
|
||||
/**
|
||||
* Optional controls rendered in the footer, left of the Done button — e.g.
|
||||
* the model selector + character button. The caller composes these from its
|
||||
* own toolbar pieces.
|
||||
*/
|
||||
footerControls?: ReactNode;
|
||||
/**
|
||||
* Optional reference-image row rendered above the footer, always visible in
|
||||
* focus mode regardless of the inline row's collapse state.
|
||||
*/
|
||||
imagePromptRow?: ReactNode;
|
||||
}
|
||||
|
||||
export const PromptFullscreenModal = ({
|
||||
@@ -28,6 +40,8 @@ export const PromptFullscreenModal = ({
|
||||
promptLength,
|
||||
maxLength,
|
||||
children,
|
||||
footerControls,
|
||||
imagePromptRow,
|
||||
}: PromptFullscreenModalProps) => {
|
||||
const overLimit = isFinite(maxLength) && promptLength > maxLength;
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
@@ -46,21 +60,26 @@ export const PromptFullscreenModal = ({
|
||||
accessibleTitle="Prompt focus mode"
|
||||
closeOnOutsideClick
|
||||
closeOnEsc
|
||||
className="h-[70vh] w-full max-w-4xl"
|
||||
className="w-full max-w-4xl"
|
||||
backdropClassName="backdrop-blur-md"
|
||||
>
|
||||
{/* Title lives inside the flex column (not the Modal title slot) so the
|
||||
editor + counter fit the fixed panel height cleanly. No overflow-hidden
|
||||
here — the mention autocomplete dropdown renders outside the editor
|
||||
bounds and must not be clipped. */}
|
||||
<div className="flex h-full flex-col gap-2">
|
||||
<h2 className="text-lg font-bold text-base-fg">Prompt</h2>
|
||||
{/* flex column so a flex-1 editor (the MentionTextarea root wrapper)
|
||||
or an h-full textarea fills the remaining space. */}
|
||||
<div ref={contentRef} className="flex min-h-0 flex-1 flex-col">
|
||||
{/* Explicit inline height (not a Tailwind arbitrary class) so the column
|
||||
is reliably bounded across the modal's nested wrappers — that's what
|
||||
lets the editor's textarea scroll instead of stretching the panel.
|
||||
min-h-0 on the flex children lets them shrink below content size. */}
|
||||
<div className="flex min-h-0 flex-col gap-2" style={{ height: "70vh" }}>
|
||||
<h2 className="shrink-0 text-lg font-bold text-base-fg">Prompt</h2>
|
||||
{/* flex-1 so the editor takes only the space left after the (shrink-0)
|
||||
image row + footer — that's what keeps it from overflowing when the
|
||||
window is shorter. min-h-0 + overflow-hidden bound the editor area so
|
||||
its own textarea scrolls instead of pushing the layout. */}
|
||||
<div
|
||||
ref={contentRef}
|
||||
className="flex min-h-0 flex-1 flex-col overflow-hidden"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex shrink-0 items-center justify-end">
|
||||
<span
|
||||
className={`text-[11px] tabular-nums ${
|
||||
overLimit ? "text-red-500" : "text-base-fg/40"
|
||||
@@ -69,6 +88,11 @@ export const PromptFullscreenModal = ({
|
||||
{promptLength} / {isFinite(maxLength) ? maxLength : "∞"}
|
||||
</span>
|
||||
</div>
|
||||
{imagePromptRow && <div className="shrink-0">{imagePromptRow}</div>}
|
||||
<div className="flex shrink-0 items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">{footerControls}</div>
|
||||
<Button onClick={onClose}>Done</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user