Browse Source

feat(11-01): improve provider badges on generate nodes

- Add Gemini sparkle logo to ProviderBadge component
- Move provider badge to left of title as prefix
- Remove "Generate Image/Video" text, show only model name
- Add titlePrefix prop to BaseNode for prepending icons
- Increase badge size from w-3 to w-3.5 for better visibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 6 months ago
parent
commit
c3c27978f9
  1. 3
      src/components/nodes/BaseNode.tsx
  2. 72
      src/components/nodes/GenerateImageNode.tsx
  3. 47
      src/components/nodes/GenerateVideoNode.tsx

3
src/components/nodes/BaseNode.tsx

@ -22,6 +22,7 @@ interface BaseNodeProps {
minWidth?: number;
minHeight?: number;
headerAction?: ReactNode;
titlePrefix?: ReactNode;
}
export function BaseNode({
@ -41,6 +42,7 @@ export function BaseNode({
minWidth = 180,
minHeight = 100,
headerAction,
titlePrefix,
}: BaseNodeProps) {
const currentNodeId = useWorkflowStore((state) => state.currentNodeId);
const groups = useWorkflowStore((state) => state.groups);
@ -201,6 +203,7 @@ export function BaseNode({
<div className="px-3 pt-2 pb-1 flex items-center justify-between">
{/* Title Section */}
<div className="flex-1 min-w-0 flex items-center gap-1.5">
{titlePrefix}
{isEditingTitle ? (
<input
ref={titleInputRef}

72
src/components/nodes/GenerateImageNode.tsx

@ -11,20 +11,24 @@ import { ModelSearchDialog } from "@/components/modals/ModelSearchDialog";
import { useToast } from "@/components/Toast";
import { getImageDimensions, calculateNodeSize } from "@/utils/nodeDimensions";
// Provider badge component - shows small provider icon for external providers
// Provider badge component - shows provider icon for all providers
function ProviderBadge({ provider }: { provider: ProviderType }) {
if (provider === "gemini") return null;
const providerName = provider === "gemini" ? "Gemini" : provider === "replicate" ? "Replicate" : "fal.ai";
return (
<span className="ml-1 text-neutral-500" title={provider === "replicate" ? "Replicate" : "fal.ai"}>
{provider === "replicate" ? (
<svg className="w-3 h-3" viewBox="0 0 1000 1000" fill="currentColor">
<span className="text-neutral-500 shrink-0" title={providerName}>
{provider === "gemini" ? (
<svg className="w-3.5 h-3.5" viewBox="0 0 65 65" fill="currentColor">
<path d="M57.8647 29.0098C52.865 26.8576 48.4905 23.905 44.7393 20.1556C40.99 16.4063 38.0373 12.0299 35.8851 7.03022C35.0589 5.11406 34.395 3.14442 33.886 1.12498C33.72 0.464747 33.128 0 32.4475 0C31.7669 0 31.1749 0.464747 31.009 1.12498C30.4999 3.14442 29.836 5.11222 29.0098 7.03022C26.8576 12.0299 23.905 16.4063 20.1556 20.1556C16.4063 23.905 12.0299 26.8576 7.03022 29.0098C5.11406 29.836 3.14442 30.4999 1.12498 31.009C0.464747 31.1749 0 31.7669 0 32.4475C0 33.128 0.464747 33.72 1.12498 33.886C3.14442 34.395 5.11222 35.0589 7.03022 35.8851C12.0299 38.0373 16.4045 40.99 20.1556 44.7393C23.9068 48.4886 26.8576 52.865 29.0098 57.8647C29.836 59.7809 30.4999 61.7505 31.009 63.7699C31.1749 64.4302 31.7669 64.8949 32.4475 64.8949C33.128 64.8949 33.72 64.4302 33.886 63.7699C34.395 61.7505 35.0589 59.7827 35.8851 57.8647C38.0373 52.865 40.99 48.4905 44.7393 44.7393C48.4886 40.99 52.865 38.0373 57.8647 35.8851C59.7809 35.0589 61.7505 34.395 63.7699 33.886C64.4302 33.72 64.8949 33.128 64.8949 32.4475C64.8949 31.7669 64.4302 31.1749 63.7699 31.009C61.7505 30.4999 59.7827 29.836 57.8647 29.0098Z" />
</svg>
) : provider === "replicate" ? (
<svg className="w-3.5 h-3.5" viewBox="0 0 1000 1000" fill="currentColor">
<polygon points="1000,427.6 1000,540.6 603.4,540.6 603.4,1000 477,1000 477,427.6" />
<polygon points="1000,213.8 1000,327 364.8,327 364.8,1000 238.4,1000 238.4,213.8" />
<polygon points="1000,0 1000,113.2 126.4,113.2 126.4,1000 0,1000 0,0" />
</svg>
) : (
<svg className="w-3 h-3" viewBox="0 0 1855 1855" fill="currentColor">
<svg className="w-3.5 h-3.5" viewBox="0 0 1855 1855" fill="currentColor">
<path fillRule="evenodd" clipRule="evenodd" d="M1181.65 78C1212.05 78 1236.42 101.947 1239.32 131.261C1265.25 392.744 1480.07 600.836 1750.02 625.948C1780.28 628.764 1805 652.366 1805 681.816V1174.18C1805 1203.63 1780.28 1227.24 1750.02 1230.05C1480.07 1255.16 1265.25 1463.26 1239.32 1724.74C1236.42 1754.05 1212.05 1778 1181.65 1778H673.354C642.951 1778 618.585 1754.05 615.678 1724.74C589.754 1463.26 374.927 1255.16 104.984 1230.05C74.7212 1227.24 50 1203.63 50 1174.18V681.816C50 652.366 74.7213 628.764 104.984 625.948C374.927 600.836 589.754 392.744 615.678 131.261C618.585 101.946 642.951 78 673.353 78H1181.65ZM402.377 926.561C402.377 1209.41 638.826 1438.71 930.501 1438.71C1222.18 1438.71 1458.63 1209.41 1458.63 926.561C1458.63 643.709 1222.18 414.412 930.501 414.412C638.826 414.412 402.377 643.709 402.377 926.561Z" />
</svg>
)}
@ -367,47 +371,38 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
const isGeminiProvider = currentProvider === "gemini";
// Dynamic title based on selected model
// Dynamic title based on selected model - just the model name
const displayTitle = useMemo(() => {
if (isGeminiOnly) {
// Gemini-only mode: just "Generate", model shown in inline dropdown
return "Generate";
} else if (nodeData.selectedModel?.displayName && nodeData.selectedModel.modelId) {
// External provider with model selected: append model name
return `Generate - ${nodeData.selectedModel.displayName}`;
if (nodeData.selectedModel?.displayName && nodeData.selectedModel.modelId) {
return nodeData.selectedModel.displayName;
}
// Fallback for legacy data or no model selected
if (nodeData.model) {
return GEMINI_IMAGE_MODELS.find(m => m.value === nodeData.model)?.label || nodeData.model;
}
return "Generate";
}, [isGeminiOnly, nodeData.selectedModel?.displayName, nodeData.selectedModel?.modelId]);
return "Select model...";
}, [nodeData.selectedModel?.displayName, nodeData.selectedModel?.modelId, nodeData.model]);
// Provider badge as title prefix
const titlePrefix = useMemo(() => (
<ProviderBadge provider={currentProvider} />
), [currentProvider]);
// Header action element based on provider mode
const headerAction = useMemo(() => {
if (isGeminiOnly) {
// Gemini-only: inline dropdown for 2 models
if (!isGeminiOnly) {
// External providers: browse button
return (
<select
value={nodeData.model}
onChange={handleModelChange}
className="nodrag nopan text-[10px] py-0.5 px-1 bg-transparent border border-neutral-600 rounded text-neutral-300 cursor-pointer hover:border-neutral-500 focus:outline-none focus:ring-1 focus:ring-neutral-500"
<button
onClick={() => setIsBrowseDialogOpen(true)}
className="nodrag nopan text-[10px] py-0.5 px-1.5 bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 transition-colors"
>
<option value="nano-banana">Nano Banana</option>
<option value="nano-banana-pro">Nano Banana Pro</option>
</select>
);
} else {
// External providers: badge + browse button
return (
<>
<ProviderBadge provider={currentProvider} />
<button
onClick={() => setIsBrowseDialogOpen(true)}
className="nodrag nopan text-[10px] py-0.5 px-1.5 bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 transition-colors"
>
Browse
</button>
</>
Browse
</button>
);
}
}, [isGeminiOnly, nodeData.model, handleModelChange, currentProvider]);
return null;
}, [isGeminiOnly]);
const isNanoBananaPro = isGeminiProvider && nodeData.model === "nano-banana-pro";
const hasCarouselImages = (nodeData.imageHistory || []).length > 1;
@ -465,6 +460,7 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
isExecuting={isRunning}
hasError={nodeData.status === "error"}
headerAction={headerAction}
titlePrefix={titlePrefix}
>
{/* Dynamic input handles based on model schema (external providers only) */}
{!isGeminiProvider && nodeData.inputSchema && nodeData.inputSchema.length > 0 ? (

47
src/components/nodes/GenerateVideoNode.tsx

@ -11,18 +11,24 @@ import { ModelSearchDialog } from "@/components/modals/ModelSearchDialog";
import { useToast } from "@/components/Toast";
import { getVideoDimensions, calculateNodeSize } from "@/utils/nodeDimensions";
// Provider badge component - shows small provider icon for external providers
// Provider badge component - shows provider icon for all providers
function ProviderBadge({ provider }: { provider: ProviderType }) {
const providerName = provider === "gemini" ? "Gemini" : provider === "replicate" ? "Replicate" : "fal.ai";
return (
<span className="ml-1 text-neutral-500" title={provider === "replicate" ? "Replicate" : "fal.ai"}>
{provider === "replicate" ? (
<svg className="w-3 h-3" viewBox="0 0 1000 1000" fill="currentColor">
<span className="text-neutral-500 shrink-0" title={providerName}>
{provider === "gemini" ? (
<svg className="w-3.5 h-3.5" viewBox="0 0 65 65" fill="currentColor">
<path d="M57.8647 29.0098C52.865 26.8576 48.4905 23.905 44.7393 20.1556C40.99 16.4063 38.0373 12.0299 35.8851 7.03022C35.0589 5.11406 34.395 3.14442 33.886 1.12498C33.72 0.464747 33.128 0 32.4475 0C31.7669 0 31.1749 0.464747 31.009 1.12498C30.4999 3.14442 29.836 5.11222 29.0098 7.03022C26.8576 12.0299 23.905 16.4063 20.1556 20.1556C16.4063 23.905 12.0299 26.8576 7.03022 29.0098C5.11406 29.836 3.14442 30.4999 1.12498 31.009C0.464747 31.1749 0 31.7669 0 32.4475C0 33.128 0.464747 33.72 1.12498 33.886C3.14442 34.395 5.11222 35.0589 7.03022 35.8851C12.0299 38.0373 16.4045 40.99 20.1556 44.7393C23.9068 48.4886 26.8576 52.865 29.0098 57.8647C29.836 59.7809 30.4999 61.7505 31.009 63.7699C31.1749 64.4302 31.7669 64.8949 32.4475 64.8949C33.128 64.8949 33.72 64.4302 33.886 63.7699C34.395 61.7505 35.0589 59.7827 35.8851 57.8647C38.0373 52.865 40.99 48.4905 44.7393 44.7393C48.4886 40.99 52.865 38.0373 57.8647 35.8851C59.7809 35.0589 61.7505 34.395 63.7699 33.886C64.4302 33.72 64.8949 33.128 64.8949 32.4475C64.8949 31.7669 64.4302 31.1749 63.7699 31.009C61.7505 30.4999 59.7827 29.836 57.8647 29.0098Z" />
</svg>
) : provider === "replicate" ? (
<svg className="w-3.5 h-3.5" viewBox="0 0 1000 1000" fill="currentColor">
<polygon points="1000,427.6 1000,540.6 603.4,540.6 603.4,1000 477,1000 477,427.6" />
<polygon points="1000,213.8 1000,327 364.8,327 364.8,1000 238.4,1000 238.4,213.8" />
<polygon points="1000,0 1000,113.2 126.4,113.2 126.4,1000 0,1000 0,0" />
</svg>
) : (
<svg className="w-3 h-3" viewBox="0 0 1855 1855" fill="currentColor">
<svg className="w-3.5 h-3.5" viewBox="0 0 1855 1855" fill="currentColor">
<path fillRule="evenodd" clipRule="evenodd" d="M1181.65 78C1212.05 78 1236.42 101.947 1239.32 131.261C1265.25 392.744 1480.07 600.836 1750.02 625.948C1780.28 628.764 1805 652.366 1805 681.816V1174.18C1805 1203.63 1780.28 1227.24 1750.02 1230.05C1480.07 1255.16 1265.25 1463.26 1239.32 1724.74C1236.42 1754.05 1212.05 1778 1181.65 1778H673.354C642.951 1778 618.585 1754.05 615.678 1724.74C589.754 1463.26 374.927 1255.16 104.984 1230.05C74.7212 1227.24 50 1203.63 50 1174.18V681.816C50 652.366 74.7213 628.764 104.984 625.948C374.927 600.836 589.754 392.744 615.678 131.261C618.585 101.946 642.951 78 673.353 78H1181.65ZM402.377 926.561C402.377 1209.41 638.826 1438.71 930.501 1438.71C1222.18 1438.71 1458.63 1209.41 1458.63 926.561C1458.63 643.709 1222.18 414.412 930.501 414.412C638.826 414.412 402.377 643.709 402.377 926.561Z" />
</svg>
)}
@ -264,27 +270,29 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps<GenerateVide
setIsBrowseDialogOpen(false);
}, [id, updateNodeData]);
// Dynamic title based on selected model
// Dynamic title based on selected model - just the model name
const displayTitle = useMemo(() => {
if (nodeData.selectedModel?.displayName && nodeData.selectedModel.modelId) {
return `Video - ${nodeData.selectedModel.displayName}`;
return nodeData.selectedModel.displayName;
}
return "Generate Video";
return "Select model...";
}, [nodeData.selectedModel?.displayName, nodeData.selectedModel?.modelId]);
// Header action element - badge + browse button
const headerAction = useMemo(() => (
<>
<ProviderBadge provider={currentProvider} />
<button
onClick={() => setIsBrowseDialogOpen(true)}
className="nodrag nopan text-[10px] py-0.5 px-1.5 bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 transition-colors"
>
Browse
</button>
</>
// Provider badge as title prefix
const titlePrefix = useMemo(() => (
<ProviderBadge provider={currentProvider} />
), [currentProvider]);
// Header action element - browse button
const headerAction = useMemo(() => (
<button
onClick={() => setIsBrowseDialogOpen(true)}
className="nodrag nopan text-[10px] py-0.5 px-1.5 bg-neutral-700 hover:bg-neutral-600 border border-neutral-600 rounded text-neutral-300 transition-colors"
>
Browse
</button>
), []);
const hasCarouselVideos = (nodeData.videoHistory || []).length > 1;
// Track previous status to detect error transitions
@ -341,6 +349,7 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps<GenerateVide
isExecuting={isRunning}
hasError={nodeData.status === "error"}
headerAction={headerAction}
titlePrefix={titlePrefix}
>
{/* Dynamic input handles based on model schema */}
{nodeData.inputSchema && nodeData.inputSchema.length > 0 ? (

Loading…
Cancel
Save