diff --git a/app/src/app/tools/[toolId]/page.tsx b/app/src/app/tools/[toolId]/page.tsx index 154a99e..3c0901c 100644 --- a/app/src/app/tools/[toolId]/page.tsx +++ b/app/src/app/tools/[toolId]/page.tsx @@ -1,9 +1,9 @@ "use client"; import { Button, Label, Textarea } from "@ansospace/ui"; -import { ArrowUpRight, Crown, Lock, Play, X } from "lucide-react"; +import { ArrowUpRight, Crown, Lock, Play, Plus, X } from "lucide-react"; import { notFound, useParams } from "next/navigation"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { CodeEditor } from "@/components/code-editor"; import { ResultViewer } from "@/components/result-viewer"; import { ToolLayout } from "@/components/tool-layout"; @@ -44,6 +44,9 @@ function ToolPageContent({ toolId }: { toolId: string }) { return initial; }); + // Length selector modal state + const [showLengthModal, setShowLengthModal] = useState(false); + // Tier2 tools MUST bypass Next.js proxy buffering to prevent silent timeouts on long executions const runnerUrl = process.env.NEXT_PUBLIC_TOOL_RUNNER_URL || "http://localhost:9080"; const apiBase = tool.tier === "tier2" ? `${runnerUrl}/api/tools` : "/api/tools"; @@ -83,23 +86,25 @@ function ToolPageContent({ toolId }: { toolId: string }) { } }, [mounted, toolUsage.limitReached]); + // Check if all required fields are filled + const isReady = tool.requiredFields.every((field) => fields[field]?.trim()); + const handleExecute = () => { - // Check per-tool usage limit if (!canExecute(tool.id)) { setShowUpgradeDialog(true); return; } - // Check required fields for (const field of tool.requiredFields) { if (!fields[field]?.trim()) return; } - execute({ ...fields, model }); - // Track usage for THIS tool - trackExecution(tool.id); - }; - // Check if all required fields are filled - const isReady = tool.requiredFields.every((field) => fields[field]?.trim()); + if (tool.requireLengthSelection) { + setShowLengthModal(true); + } else { + execute({ ...fields, model }); + trackExecution(tool.id); + } + }; return ( <> @@ -117,6 +122,7 @@ function ToolPageContent({ toolId }: { toolId: string }) { config={input} value={fields[input.key] || ""} onChange={(value) => setField(input.key, value)} + onFieldChange={(key, value) => setField(key, value)} /> ))} @@ -202,11 +208,66 @@ function ToolPageContent({ toolId }: { toolId: string }) { {/* Results */}
- + {tool.ResultComponent && result ? ( + + ) : ( + + )}
+ {/* Length selector modal (for tools that require it) */} + {tool.requireLengthSelection && showLengthModal && ( +
+
+ + +
+ +
+ +

Caption Length

+

+ How long do you want your captions to be? +

+ +
+ + +
+
+
+ )} + {/* ─── Upgrade Dialog Popup ──────────────────────────── */} {showUpgradeDialog && (
@@ -297,11 +358,18 @@ function InputField({ config, value, onChange, + onFieldChange, }: { config: InputFieldConfig; value: string; onChange: (value: string) => void; + onFieldChange?: (key: string, value: string) => void; }) { + const textareaFileRef = useRef(null); + const [textareaShowPreview, setTextareaShowPreview] = useState(false); + const [textareaSpinning, setTextareaSpinning] = useState(false); + const [textareaAttachedImage, setTextareaAttachedImage] = useState(""); + switch (config.type) { case "code": return ( @@ -316,19 +384,103 @@ function InputField({
); - case "textarea": + case "textarea": { + const hasImage = config.attachable && !!textareaAttachedImage; return (
-