Enforce MAX_USER_PROMPT limit in generate-page-modal and comic-creation-form

This commit is contained in:
Riccardo Giorato
2026-02-11 11:37:28 +01:00
parent a05fdea297
commit 9cf3e31af2
3 changed files with 58 additions and 15 deletions
+13 -7
View File
@@ -15,6 +15,7 @@ import { useKeyboardShortcut } from "@/hooks/use-keyboard-shortcut";
import { validateFileForUpload, generateFilePreview } from "@/lib/file-utils"; import { validateFileForUpload, generateFilePreview } from "@/lib/file-utils";
import { useS3Upload } from "next-s3-upload"; import { useS3Upload } from "next-s3-upload";
import { isContentPolicyViolation } from "@/lib/utils"; import { isContentPolicyViolation } from "@/lib/utils";
import { MAX_SYSTEM_LENGTH, MAX_USER_PROMPT } from "@/lib/prompt";
interface CharacterItem { interface CharacterItem {
url: string; url: string;
@@ -330,13 +331,19 @@ export function GeneratePageModal({
<textarea <textarea
autoFocus autoFocus
value={prompt} value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onChange={(e) => setPrompt(e.target.value.slice(
// Only allow users to type up to MAX_SYSTEM_LENGTH characters in the prompt.
// Ensure users cannot paste or otherwise enter more text than the max.
0, MAX_USER_PROMPT))
}
placeholder={ placeholder={
isRedrawMode isRedrawMode
? "Tweak the prompt to improve this page..." ? "Tweak the prompt to improve this page..."
: "Continue the story... Describe what happens next." : "Continue the story... Describe what happens next."
} }
disabled={isGenerating} disabled={isGenerating}
maxLength={MAX_USER_PROMPT}
className="w-full bg-transparent border-none text-sm text-white placeholder-muted-foreground/50 focus:ring-0 focus:outline-none resize-none h-20 leading-relaxed tracking-tight" className="w-full bg-transparent border-none text-sm text-white placeholder-muted-foreground/50 focus:ring-0 focus:outline-none resize-none h-20 leading-relaxed tracking-tight"
/> />
@@ -358,11 +365,10 @@ export function GeneratePageModal({
onClick={() => toggleCharacterSelection(index)} onClick={() => toggleCharacterSelection(index)}
onDoubleClick={() => setShowPreview(imageUrl)} onDoubleClick={() => setShowPreview(imageUrl)}
disabled={isGenerating} disabled={isGenerating}
className={`w-10 h-10 rounded-md overflow-hidden transition-all disabled:opacity-50 disabled:cursor-not-allowed relative ${ className={`w-10 h-10 rounded-md overflow-hidden transition-all disabled:opacity-50 disabled:cursor-not-allowed relative ${isSelected
isSelected ? "border-2 border-indigo-500"
? "border-2 border-indigo-500" : "border-2 border-transparent hover:border-indigo/50"
: "border-2 border-transparent hover:border-indigo/50" }`}
}`}
title="Click to select/deselect, double-click to preview" title="Click to select/deselect, double-click to preview"
> >
<img <img
@@ -432,7 +438,7 @@ export function GeneratePageModal({
{isRedrawMode {isRedrawMode
? "Previous pages and characters automatically referenced." ? "Previous pages and characters automatically referenced."
: "Previous page automatically referenced. " + : "Previous page automatically referenced. " +
`${selectedCharacterIndices.size} selected characters.`} `${selectedCharacterIndices.size} selected characters.`}
</div> </div>
<Button <Button
+7 -3
View File
@@ -12,6 +12,7 @@ import { useKeyboardShortcut } from "@/hooks/use-keyboard-shortcut";
import { useApiKey } from "@/hooks/use-api-key"; import { useApiKey } from "@/hooks/use-api-key";
import { isContentPolicyViolation } from "@/lib/utils"; import { isContentPolicyViolation } from "@/lib/utils";
import { ApiKeyModal } from "@/components/api-key-modal"; import { ApiKeyModal } from "@/components/api-key-modal";
import { MAX_SYSTEM_LENGTH, MAX_USER_PROMPT } from "@/lib/prompt";
interface ComicCreationFormProps { interface ComicCreationFormProps {
prompt: string; prompt: string;
@@ -328,9 +329,12 @@ export function ComicCreationForm({
<textarea <textarea
ref={textareaRef} ref={textareaRef}
value={prompt} value={prompt}
onChange={(e) => setPrompt(e.target.value)} onChange={(e) => setPrompt(e.target.value.slice(
0, MAX_USER_PROMPT))
}
placeholder="A cyberpunk detective standing in neon rain, holding a glowing datapad, moody lighting, noir style..." placeholder="A cyberpunk detective standing in neon rain, holding a glowing datapad, moody lighting, noir style..."
disabled={isLoading} disabled={isLoading}
maxLength={MAX_USER_PROMPT}
className="w-full bg-transparent border-none text-sm text-white placeholder-muted-foreground/50 focus:ring-0 focus:outline-none resize-none h-16 leading-relaxed disabled:opacity-50 disabled:cursor-not-allowed" className="w-full bg-transparent border-none text-sm text-white placeholder-muted-foreground/50 focus:ring-0 focus:outline-none resize-none h-16 leading-relaxed disabled:opacity-50 disabled:cursor-not-allowed"
/> />
@@ -424,8 +428,8 @@ export function ComicCreationForm({
setShowStyleDropdown(false); setShowStyleDropdown(false);
}} }}
className={`w-full text-left px-3 py-2 rounded text-xs transition-colors flex items-center justify-between ${style === styleOption.id className={`w-full text-left px-3 py-2 rounded text-xs transition-colors flex items-center justify-between ${style === styleOption.id
? "bg-indigo/10 text-indigo" ? "bg-indigo/10 text-indigo"
: "text-muted-foreground hover:bg-white/5 hover:text-white" : "text-muted-foreground hover:bg-white/5 hover:text-white"
}`} }`}
> >
<span>{styleOption.name}</span> <span>{styleOption.name}</span>
+37 -4
View File
@@ -1,5 +1,12 @@
import { COMIC_STYLES } from "./constants"; import { COMIC_STYLES } from "./constants";
// Together AI has a 45000 character limit for the prompt parameter
// We use 40k total to keep a larger safety margin
export const MAX_PROMPT_LENGTH = 40000;
export const MAX_SYSTEM_LENGTH = 35000; // Reserve 5,000 for user's prompt
export const MAX_USER_PROMPT = MAX_PROMPT_LENGTH - MAX_SYSTEM_LENGTH;
export function buildComicPrompt({ export function buildComicPrompt({
prompt, prompt,
style, style,
@@ -28,11 +35,37 @@ export function buildComicPrompt({
} }
if (isAddPage && previousPages.length > 0) { if (isAddPage && previousPages.length > 0) {
const storyHistory = previousPages // Limit previous pages to fit within MAX_SYSTEM_LENGTH
.map((page, index) => `Page ${index + 1}: ${page.prompt}`) // Start with most recent pages and work backwards
.join("\n"); const header = `\nSTORY CONTINUATION CONTEXT:\nThis is page ${previousPages.length + 1} of an existing comic story. Here are the recent pages for context:\n`;
const footer = `\n\nThe new page should naturally continue this story. Maintain the same characters, setting, and narrative style. Reference previous events and build upon them.\n`;
continuationContext = `\nSTORY CONTINUATION CONTEXT:\nThis is a continuation of an existing comic story. Here are the previous pages:\n${storyHistory}\n\nThe new page should naturally continue this story. Maintain the same characters, setting, and narrative style. Reference previous events and build upon them.\n`; // Calculate available space for previous pages (reserve space for rest of system prompt)
const basePromptLength = 2500; // Approximate length of system prompt without previous pages
const availableSpace = MAX_SYSTEM_LENGTH - basePromptLength - header.length - footer.length;
const selectedPages: string[] = [];
let currentLength = 0;
// Add pages from most recent backwards until we run out of space
for (let i = previousPages.length - 1; i >= 0; i--) {
const pageEntry = `Page ${i + 1}: ${previousPages[i].prompt}`;
const entryLength = pageEntry.length + (selectedPages.length > 0 ? 1 : 0); // +1 for newline
if (currentLength + entryLength <= availableSpace) {
selectedPages.unshift(pageEntry);
currentLength += entryLength;
} else {
break;
}
}
if (selectedPages.length > 0) {
continuationContext = header + selectedPages.join("\n") + footer;
} else {
// Fallback if no pages fit
continuationContext = `\nSTORY CONTINUATION CONTEXT:\nThis is page ${previousPages.length + 1} of an existing comic story with ${previousPages.length} previous pages. Continue the story maintaining consistency.\n`;
}
} }
let characterSection = ""; let characterSection = "";