Repository: giselles-ai/giselle
Pull request #721: Added support for connecting image nodes to Text Generation nodes using OpenAI models
Author: login: toyamarinyon
Created: 2025-04-16T04:32:21Z | Merged: 2025-04-16T07:20:14Z

This PR enables connecting image nodes to Text Generation nodes when an OpenAI model is selected.

## Changes

- Added support for image-to-text connections with OpenAI models
- Reorganized components and variable names in the Input tab implementation to make them more intuitive and less confusing
- Implemented a simplified approach for connection compatibility (while ideally this should be comprehensively determined based on information defined in the LanguageModel package)

## Test Method

1. Access the preview environment and create a Text Generation node using the OpenAI model.
2. Create an Image node.
3. Connect the Image node to the Text Generation node.
4. Enter "Describe the components of the image" in the Text Generation node and generate content.
5. The text describing the components of the image will be displayed.


Diff:
diff --git a/.claude/commands/update-branch-name.md b/.claude/commands/update-branch-name.md
new file mode 100644
index 0000000000..1fb2fe0c2f
--- /dev/null
+++ b/.claude/commands/update-branch-name.md
@@ -0,0 +1,9 @@
+# Update Branch Name
+
+Follow these steps to update the current branch name:
+
+1. Check differences between current branch and main branch HEAD using `git diff main...HEAD`
+2. Analyze the changed files to understand what work is being done
+3. Determine an appropriate descriptive branch name based on the changes
+4. Update the current branch name using `git branch -m [new-branch-name]`
+5. Verify the branch name was updated with `git branch`
diff --git a/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/index.tsx b/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/index.tsx
index 06b9ec59d3..d6c5ca29e8 100644
--- a/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/index.tsx
+++ b/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/index.tsx
@@ -31,8 +31,8 @@ import {
 	OpenAIModelPanel,
 	PerplexityModelPanel,
 } from "./model";
+import { useConnectedOutputs } from "./outputs";
 import { PromptPanel } from "./prompt-panel";
-import { useConnectedSources } from "./sources";
 import { GitHubToolsPanel } from "./tools";
 
 export function TextGenerationNodePropertiesPanel({
@@ -51,7 +51,7 @@ export function TextGenerationNodePropertiesPanel({
 		nodeId: node.id,
 		origin: { type: "workspace", id: data.id },
 	});
-	const { all: connectedSources } = useConnectedSources(node);
+	const { all: connectedSources } = useConnectedOutputs(node);
 	const usageLimitsReached = useUsageLimitsReached();
 	const { error } = useToasts();
 
diff --git a/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/input-panel.tsx b/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/input-panel.tsx
index 7412109c87..74fffa1ea5 100644
--- a/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/input-panel.tsx
+++ b/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/input-panel.tsx
@@ -2,22 +2,13 @@ import {
 	type Connection,
 	type Input,
 	InputId,
-	OutputId,
+	type OutputId,
 	type TextGenerationNode,
 } from "@giselle-sdk/data-type";
 import { isJsonContent, jsonContentToText } from "@giselle-sdk/text-editor";
-import clsx from "clsx/lite";
 import { useWorkflowDesigner } from "giselle-sdk/react";
-import { CheckIcon, TrashIcon } from "lucide-react";
 import pluralize from "pluralize";
-import { Popover, ToggleGroup } from "radix-ui";
-import {
-	type ComponentProps,
-	type ReactNode,
-	useCallback,
-	useMemo,
-	useState,
-} from "react";
+import { useCallback, useMemo } from "react";
 import {
 	GeneratedContentIcon,
 	GitHubIcon,
@@ -26,272 +17,12 @@ import {
 } from "../../../icons";
 import { EmptyState } from "../../../ui/empty-state";
 import {
-	type Source,
-	useConnectedSources,
-	useSourceCategories,
-} from "./sources";
-
-function SourceToggleItem({
-	source,
-	disabled = false,
-}: { source: Source; disabled?: boolean }) {
-	const getDisplayName = () => {
-		if ("content" in source.node && "llm" in source.node.content) {
-			return source.node.name ?? source.node.content.llm.id;
-		}
-		return source.node.name ?? "Source";
-	};
-
-	return (
-		<ToggleGroup.Item
-			key={source.output.id}
-			className={clsx(
-				"group flex p-[8px] justify-between rounded-[8px] hover:bg-primary-900/50 transition-colors cursor-pointer",
-				"text-white-400",
-				"data-[disabled]:text-white-850/30 data-[disabled]:pointer-events-none",
-			)}
-			value={source.output.id}
-			disabled={disabled}
-		>
-			<p className="text-[12px] truncate">
-				{getDisplayName()} / {source.output.label}
-			</p>
-			<CheckIcon className="w-[16px] h-[16px] hidden group-data-[state=on]:block" />
-			<div
-				className={clsx(
-					"px-[10px] py-[4px] flex items-center justify-center rounded-[30px]",
-					"bg-black-200/20 text-black-200/20 text-[10px]",
-					"hidden group-data-[disabled]:block",
-				)}
-			>
-				Unsupported
-			</div>
-		</ToggleGroup.Item>
-	);
-}
-
-function SourceSelect({
-	node,
-	sources,
-	onValueChange,
-	contentProps,
-}: {
-	node: TextGenerationNode;
-	sources: Source[];
-	onValueChange?: (value: OutputId[]) => void;
-	contentProps?: Omit<
-		ComponentProps<typeof Popover.PopoverContent>,
-		"className"
-	>;
-}) {
-	const [selectedOutputIds, setSelectedOutputIds] = useState<OutputId[]>([]);
-	const { generatedSources, textSources, fileSources, githubSources } =
-		useSourceCategories(sources);
-	const { isSupportedConnection } = useWorkflowDesigner();
-	const isSupported = useCallback(
-		(source: Source) => {
-			const { canConnect } = isSupportedConnection(source.node, node);
-			return canConnect;
-		},
-		[isSupportedConnection, node],
-	);
-
-	return (
-		<Popover.Root
-			onOpenChange={(open) => {
-				if (open) {
-					setSelectedOutputIds(
-						sources
-							.filter((source) => source.connection !== undefined)
-							.map((source) => source.output.id),
-					);
-				}
-			}}
-		>
-			<Popover.Trigger
-				className={clsx(
-					"flex items-center cursor-pointer p-[10px] rounded-[8px]",
-					"border border-transparent hover:border-white-800",
-					"text-[12px] font-[700] text-white-800",
-					"transition-colors",
-				)}
-			>
-				Select Sources
-			</Popover.Trigger>
-			<Popover.Anchor />
-			<Popover.Portal>
-				<Popover.Content
-					className={clsx(
-						"relative w-[300px] py-[8px]",
-						"rounded-[8px] border-[1px] bg-black-900/60 backdrop-blur-[8px]",
-						"shadow-[-2px_-1px_0px_0px_rgba(0,0,0,0.1),1px_1px_8px_0px_rgba(0,0,0,0.25)]",
-					)}
-					{...contentProps}
-				>
-					<div
-						className={clsx(
-							"absolute z-0 rounded-[8px] inset-0 border-[1px] mask-fill bg-gradient-to-br bg-origin-border bg-clip-boarder border-transparent",
-							"from-[hsl(232,_36%,_72%)]/40 to-[hsl(218,_58%,_21%)]/90",
-						)}
-					/>
-					<ToggleGroup.Root
-						type="multiple"
-						className="relative max-h-[300px] flex flex-col"
-						value={selectedOutputIds}
-						onValueChange={(unsafeValue) => {
-							const safeValue = unsafeValue
-								.map((value) => {
-									const parse = OutputId.safeParse(value);
-									if (parse.success) {
-										return parse.data;
-									}
-									return null;
-								})
-								.filter((id) => id !== null);
-							setSelectedOutputIds(safeValue);
-						}}
-					>
-						<div className="flex px-[16px] text-white-900">
-							Select Sources From
-						</div>
-						<div className="flex flex-col py-[4px]">
-							<div className="border-t border-black-300/20" />
-						</div>
-						<div className="grow flex flex-col pb-[8px] gap-[8px] overflow-y-auto min-h-0">
-							{generatedSources.length > 0 && (
-								<div className="flex flex-col px-[8px]">
-									<p className="py-[4px] px-[8px] text-black-400 text-[10px] font-[700]">
-										Generated Content
-									</p>
-									{generatedSources.map((generatedSource) => (
-										<SourceToggleItem
-											key={generatedSource.output.id}
-											source={generatedSource}
-											disabled={!isSupported(generatedSource)}
-										/>
-									))}
-								</div>
-							)}
-							{textSources.length > 0 && (
-								<div className="flex flex-col px-[8px]">
-									<p className="py-[4px] px-[8px] text-black-400 text-[10px] font-[700]">
-										Text
-									</p>
-									{textSources.map((textSource) => (
-										<SourceToggleItem
-											key={textSource.output.id}
-											source={textSource}
-											disabled={!isSupported(textSource)}
-										/>
-									))}
-								</div>
-							)}
-
-							{fileSources.length > 0 && (
-								<div className="flex flex-col px-[8px]">
-									<p className="py-[4px] px-[8px] text-black-400 text-[10px] font-[700]">
-										File
-									</p>
-									{fileSources.map((fileSource) => (
-										<SourceToggleItem
-											key={fileSource.output.id}
-											source={fileSource}
-											disabled={!isSupported(fileSource)}
-										/>
-									))}
-								</div>
-							)}
-							{githubSources.length > 0 && (
-								<div className="flex flex-col px-[8px]">
-									<p className="py-[4px] px-[8px] text-black-400 text-[10px] font-[700]">
-										GitHub
-									</p>
-									{githubSources.map((githubSource) => (
-										<SourceToggleItem
-											key={githubSource.output.id}
-											source={githubSource}
-											disabled={!isSupported(githubSource)}
-										/>
-									))}
-								</div>
-							)}
-						</div>
-						<div className="flex flex-col py-[4px]">
-							<div className="border-t border-black-300/20" />
-						</div>
-						<div className="flex px-[16px] py-[4px] gap-[8px]">
-							<Popover.Close
-								onClick={() => {
-									onValueChange?.(selectedOutputIds);
-								}}
-								className="h-[32px] w-full flex justify-center items-center bg-white-900 text-black-900 rounded-[8px] cursor-pointer text-[12px]"
-							>
-								Update
-							</Popover.Close>
-						</div>
-					</ToggleGroup.Root>
-				</Popover.Content>
-			</Popover.Portal>
-		</Popover.Root>
-	);
-}
-
-function SourceListRoot({
-	title,
-	children,
-}: {
-	title: string;
-	children: ReactNode;
-}) {
-	return (
-		<div className="flex flex-col gap-[8px]">
-			<p className="text-[14px]">{title}</p>
-			{children}
-		</div>
-	);
-}
-function SourceListItem({
-	icon,
-	title,
-	subtitle,
-	onRemove,
-}: {
-	icon: ReactNode;
-	title: string;
-	subtitle: string;
-	onRemove: () => void;
-}) {
-	return (
-		<div
-			className={clsx(
-				"group flex items-center",
-				"border border-white-900/20 rounded-[8px] h-[60px]",
-			)}
-		>
-			<div className="w-[60px] flex items-center justify-center">{icon}</div>
-			<div className="w-[1px] h-full border-l border-white-900/20" />
-			<div className="px-[16px] flex-1 flex items-center justify-between">
-				<div className="flex flex-col gap-[4px]">
-					<p className="text=[16px]">{title}</p>
-					<div className="text-[10px] text-black-400">
-						<p className="line-clamp-1">{subtitle}</p>
-					</div>
-				</div>
-				<button
-					type="button"
-					className={clsx(
-						"hidden group-hover:block",
-						"p-[4px] rounded-[4px]",
-						"bg-transparent hover:bg-black-300/50 transition-colors",
-					)}
-					onClick={onRemove}
-				>
-					<TrashIcon className="w-[18px] h-[18px] text-white-900" />
-				</button>
-			</div>
-		</div>
-	);
-}
+	ConnectedOutputListItem,
+	ConnectedOutputListRoot,
+	type OutputWithDetails,
+	SelectOutputPopover,
+	useConnectedOutputs,
+} from "./outputs";
 
 export function InputPanel({
 	node: textGenerationNode,
@@ -300,9 +31,9 @@ export function InputPanel({
 }) {
 	const { data, addConnection, deleteConnection, updateNodeData } =
 		useWorkflowDesigner();
-	const sources = useMemo<Source[]>(() => {
-		const tmpSources: Source[] = [];
-		const connections = data.connections.filter(
+	const outputs = useMemo<OutputWithDetails[]>(() => {
+		const tmp: OutputWithDetails[] = [];
+		const connectionToThisNode = data.connections.filter(
 			(connection) => connection.inputNode.id === textGenerationNode.id,
 		);
 		for (const node of data.nodes) {
@@ -310,19 +41,19 @@ export function InputPanel({
 				continue;
 			}
 			for (const output of node.outputs) {
-				const connection = connections.find(
+				const connection = connectionToThisNode.find(
 					(connection) => connection.outputId === output.id,
 				);
-				tmpSources.push({
-					output,
+				tmp.push({
+					...output,
 					node,
 					connection,
 				});
 			}
 		}
-		return tmpSources;
+		return tmp;
 	}, [data.nodes, data.connections, textGenerationNode.id]);
-	const connectedSources = useConnectedSources(textGenerationNode);
+	const connectedOutputs = useConnectedOutputs(textGenerationNode);
 
 	const handleConnectionChange = useCallback(
 		(connectOutputIds: OutputId[]) => {
@@ -414,9 +145,9 @@ export function InputPanel({
 					title="No data referenced yet."
 					description="Select the data you want to refer to from the output and the information and knowledge you have."
 				>
-					<SourceSelect
+					<SelectOutputPopover
 						node={textGenerationNode}
-						sources={sources}
+						outputs={outputs}
 						onValueChange={handleConnectionChange}
 					/>
 				</EmptyState>
@@ -426,9 +157,9 @@ export function InputPanel({
 	return (
 		<div>
 			<div className="flex justify-end">
-				<SourceSelect
+				<SelectOutputPopover
 					node={textGenerationNode}
-					sources={sources}
+					outputs={outputs}
 					onValueChange={handleConnectionChange}
 					contentProps={{
 						align: "end",
@@ -436,24 +167,24 @@ export function InputPanel({
 				/>
 			</div>
 			<div className="flex flex-col gap-[32px]">
-				{connectedSources.generation.length > 0 && (
-					<SourceListRoot title="Generated Sources">
-						{connectedSources.generation.map((source) => (
-							<SourceListItem
+				{connectedOutputs.generation.length > 0 && (
+					<ConnectedOutputListRoot title="Generated Sources">
+						{connectedOutputs.generation.map((source) => (
+							<ConnectedOutputListItem
 								icon={
 									<GeneratedContentIcon className="size-[24px] text-white-900" />
 								}
 								key={source.connection.id}
-								title={`${source.node.name ?? source.node.content.llm.id} / ${source.output.label}`}
+								title={`${source.node.name ?? source.node.content.llm.id} / ${source.label}`}
 								subtitle={source.node.content.llm.provider}
 								onRemove={() => handleRemove(source.connection)}
 							/>
 						))}
-					</SourceListRoot>
+					</ConnectedOutputListRoot>
 				)}
-				{connectedSources.variable.length > 0 && (
-					<SourceListRoot title="Static Contents">
-						{connectedSources.variable.map((source) => {
+				{connectedOutputs.variable.length > 0 && (
+					<ConnectedOutputListRoot title="Static Contents">
+						{connectedOutputs.variable.map((source) => {
 							switch (source.node.content.type) {
 								case "text": {
 									let text = source.node.content.text;
@@ -467,12 +198,12 @@ export function InputPanel({
 									}
 
 									return (
-										<SourceListItem
+										<ConnectedOutputListItem
 											icon={
 												<PromptIcon className="size-[24px] text-white-900" />
 											}
 											key={source.connection.id}
-											title={`${source.node.name ?? "Text"} / ${source.output.label}`}
+											title={`${source.node.name ?? "Text"} / ${source.label}`}
 											subtitle={text}
 											onRemove={() => handleRemove(source.connection)}
 										/>
@@ -480,24 +211,24 @@ export function InputPanel({
 								}
 								case "file":
 									return (
-										<SourceListItem
+										<ConnectedOutputListItem
 											icon={
 												<PdfFileIcon className="size-[24px] text-white-900" />
 											}
 											key={source.connection.id}
-											title={`${source.node.name ?? "PDF Files"} / ${source.output.label}`}
+											title={`${source.node.name ?? "PDF Files"} / ${source.label}`}
 											subtitle={`${source.node.content.files.length} ${pluralize("file", source.node.content.files.length)}`}
 											onRemove={() => handleRemove(source.connection)}
 										/>
 									);
 								case "github":
 									return (
-										<SourceListItem
+										<ConnectedOutputListItem
 											icon={
 												<GitHubIcon className="size-[24px] text-white-900" />
 											}
 											key={source.connection.id}
-											title={`${source.node.name ?? "GitHub"} / ${source.output.label}`}
+											title={`${source.node.name ?? "GitHub"} / ${source.label}`}
 											subtitle={"todo"}
 											onRemove={() => handleRemove(source.connection)}
 										/>
@@ -508,7 +239,7 @@ export function InputPanel({
 								}
 							}
 						})}
-					</SourceListRoot>
+					</ConnectedOutputListRoot>
 				)}
 			</div>
 		</div>
diff --git a/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/outputs/components.tsx b/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/outputs/components.tsx
new file mode 100644
index 0000000000..f883fefdd7
--- /dev/null
+++ b/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/outputs/components.tsx
@@ -0,0 +1,275 @@
+import { OutputId, type TextGenerationNode } from "@giselle-sdk/data-type";
+import clsx from "clsx/lite";
+import { useWorkflowDesigner } from "giselle-sdk/react";
+import { CheckIcon, TrashIcon } from "lucide-react";
+import { Popover, ToggleGroup } from "radix-ui";
+import {
+	type ComponentProps,
+	type ReactNode,
+	useCallback,
+	useState,
+} from "react";
+import type { OutputWithDetails } from "./types";
+import { useCategoriedOutputs } from "./use-categoried-outputs";
+
+function OutputToggleItem({
+	input,
+	disabled = false,
+}: { input: OutputWithDetails; disabled?: boolean }) {
+	const getDisplayName = () => {
+		if ("content" in input.node && "llm" in input.node.content) {
+			return input.node.name ?? input.node.content.llm.id;
+		}
+		return input.node.name ?? "Source";
+	};
+
+	return (
+		<ToggleGroup.Item
+			key={input.id}
+			className={clsx(
+				"group flex p-[8px] justify-between rounded-[8px] hover:bg-primary-900/50 transition-colors cursor-pointer",
+				"text-white-400",
+				"data-[disabled]:text-white-850/30 data-[disabled]:pointer-events-none",
+			)}
+			value={input.id}
+			disabled={disabled}
+		>
+			<p className="text-[12px] truncate">
+				{getDisplayName()} / {input.label}
+			</p>
+			<CheckIcon className="w-[16px] h-[16px] hidden group-data-[state=on]:block" />
+			<div
+				className={clsx(
+					"px-[10px] py-[4px] flex items-center justify-center rounded-[30px]",
+					"bg-black-200/20 text-black-200/20 text-[10px]",
+					"hidden group-data-[disabled]:block",
+				)}
+			>
+				Unsupported
+			</div>
+		</ToggleGroup.Item>
+	);
+}
+
+export function SelectOutputPopover({
+	node,
+	outputs,
+	onValueChange,
+	contentProps,
+}: {
+	node: TextGenerationNode;
+	outputs: OutputWithDetails[];
+	onValueChange?: (value: OutputId[]) => void;
+	contentProps?: Omit<
+		ComponentProps<typeof Popover.PopoverContent>,
+		"className"
+	>;
+}) {
+	const [selectedOutputIds, setSelectedOutputIds] = useState<OutputId[]>([]);
+	const { generatedInputs, textInputs, fileInputs, githubInputs } =
+		useCategoriedOutputs(outputs);
+	const { isSupportedConnection } = useWorkflowDesigner();
+	const isSupported = useCallback(
+		(input: OutputWithDetails) => {
+			const { canConnect } = isSupportedConnection(input.node, node);
+			return canConnect;
+		},
+		[isSupportedConnection, node],
+	);
+
+	return (
+		<Popover.Root
+			onOpenChange={(open) => {
+				if (open) {
+					setSelectedOutputIds(
+						outputs
+							.filter((input) => input.connection !== undefined)
+							.map((input) => input.id),
+					);
+				}
+			}}
+		>
+			<Popover.Trigger
+				className={clsx(
+					"flex items-center cursor-pointer p-[10px] rounded-[8px]",
+					"border border-transparent hover:border-white-800",
+					"text-[12px] font-[700] text-white-800",
+					"transition-colors",
+				)}
+			>
+				Select Sources
+			</Popover.Trigger>
+			<Popover.Anchor />
+			<Popover.Portal>
+				<Popover.Content
+					className={clsx(
+						"relative w-[300px] py-[8px]",
+						"rounded-[8px] border-[1px] bg-black-900/60 backdrop-blur-[8px]",
+						"shadow-[-2px_-1px_0px_0px_rgba(0,0,0,0.1),1px_1px_8px_0px_rgba(0,0,0,0.25)]",
+					)}
+					{...contentProps}
+				>
+					<div
+						className={clsx(
+							"absolute z-0 rounded-[8px] inset-0 border-[1px] mask-fill bg-gradient-to-br bg-origin-border bg-clip-boarder border-transparent",
+							"from-[hsl(232,_36%,_72%)]/40 to-[hsl(218,_58%,_21%)]/90",
+						)}
+					/>
+					<ToggleGroup.Root
+						type="multiple"
+						className="relative max-h-[300px] flex flex-col"
+						value={selectedOutputIds}
+						onValueChange={(unsafeValue) => {
+							const safeValue = unsafeValue
+								.map((value) => {
+									const parse = OutputId.safeParse(value);
+									if (parse.success) {
+										return parse.data;
+									}
+									return null;
+								})
+								.filter((id) => id !== null);
+							setSelectedOutputIds(safeValue);
+						}}
+					>
+						<div className="flex px-[16px] text-white-900">
+							Select Sources From
+						</div>
+						<div className="flex flex-col py-[4px]">
+							<div className="border-t border-black-300/20" />
+						</div>
+						<div className="grow flex flex-col pb-[8px] gap-[8px] overflow-y-auto min-h-0">
+							{generatedInputs.length > 0 && (
+								<div className="flex flex-col px-[8px]">
+									<p className="py-[4px] px-[8px] text-black-400 text-[10px] font-[700]">
+										Generated Content
+									</p>
+									{generatedInputs.map((generatedInput) => (
+										<OutputToggleItem
+											key={generatedInput.id}
+											input={generatedInput}
+											disabled={!isSupported(generatedInput)}
+										/>
+									))}
+								</div>
+							)}
+							{textInputs.length > 0 && (
+								<div className="flex flex-col px-[8px]">
+									<p className="py-[4px] px-[8px] text-black-400 text-[10px] font-[700]">
+										Text
+									</p>
+									{textInputs.map((textInput) => (
+										<OutputToggleItem
+											key={textInput.id}
+											input={textInput}
+											disabled={!isSupported(textInput)}
+										/>
+									))}
+								</div>
+							)}
+
+							{fileInputs.length > 0 && (
+								<div className="flex flex-col px-[8px]">
+									<p className="py-[4px] px-[8px] text-black-400 text-[10px] font-[700]">
+										File
+									</p>
+									{fileInputs.map((fileInput) => (
+										<OutputToggleItem
+											key={fileInput.id}
+											input={fileInput}
+											disabled={!isSupported(fileInput)}
+										/>
+									))}
+								</div>
+							)}
+							{githubInputs.length > 0 && (
+								<div className="flex flex-col px-[8px]">
+									<p className="py-[4px] px-[8px] text-black-400 text-[10px] font-[700]">
+										GitHub
+									</p>
+									{githubInputs.map((githubInput) => (
+										<OutputToggleItem
+											key={githubInput.id}
+											input={githubInput}
+											disabled={!isSupported(githubInput)}
+										/>
+									))}
+								</div>
+							)}
+						</div>
+						<div className="flex flex-col py-[4px]">
+							<div className="border-t border-black-300/20" />
+						</div>
+						<div className="flex px-[16px] py-[4px] gap-[8px]">
+							<Popover.Close
+								onClick={() => {
+									onValueChange?.(selectedOutputIds);
+								}}
+								className="h-[32px] w-full flex justify-center items-center bg-white-900 text-black-900 rounded-[8px] cursor-pointer text-[12px]"
+							>
+								Update
+							</Popover.Close>
+						</div>
+					</ToggleGroup.Root>
+				</Popover.Content>
+			</Popover.Portal>
+		</Popover.Root>
+	);
+}
+
+export function ConnectedOutputListRoot({
+	title,
+	children,
+}: {
+	title: string;
+	children: ReactNode;
+}) {
+	return (
+		<div className="flex flex-col gap-[8px]">
+			<p className="text-[14px]">{title}</p>
+			{children}
+		</div>
+	);
+}
+export function ConnectedOutputListItem({
+	icon,
+	title,
+	subtitle,
+	onRemove,
+}: {
+	icon: ReactNode;
+	title: string;
+	subtitle: string;
+	onRemove: () => void;
+}) {
+	return (
+		<div
+			className={clsx(
+				"group flex items-center",
+				"border border-white-900/20 rounded-[8px] h-[60px]",
+			)}
+		>
+			<div className="w-[60px] flex items-center justify-center">{icon}</div>
+			<div className="w-[1px] h-full border-l border-white-900/20" />
+			<div className="px-[16px] flex-1 flex items-center justify-between">
+				<div className="flex flex-col gap-[4px]">
+					<p className="text-[16px]">{title}</p>
+					<div className="text-[10px] text-black-400">
+						<p className="line-clamp-1">{subtitle}</p>
+					</div>
+				</div>
+				<button
+					type="button"
+					className={clsx(
+						"hidden group-hover:block",
+						"p-[4px] rounded-[4px]",
+						"bg-transparent hover:bg-black-300/50 transition-colors",
+					)}
+					onClick={onRemove}
+				>
+					<TrashIcon className="w-[18px] h-[18px] text-white-900" />
+				</button>
+			</div>
+		</div>
+	);
+}
diff --git a/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/outputs/index.ts b/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/outputs/index.ts
new file mode 100644
index 0000000000..7d4d75f57a
--- /dev/null
+++ b/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/outputs/index.ts
@@ -0,0 +1,5 @@
+export * from "./types";
+export * from "./utils";
+export * from "./use-connected-outputs";
+export * from "./use-categoried-outputs";
+export * from "./components";
diff --git a/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/outputs/types.ts b/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/outputs/types.ts
new file mode 100644
index 0000000000..36f4ada534
--- /dev/null
+++ b/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/outputs/types.ts
@@ -0,0 +1,18 @@
+import type {
+	Connection,
+	Node,
+	NodeBase,
+	Output,
+} from "@giselle-sdk/data-type";
+
+export type UnconnectedOutputWithDetails<T extends NodeBase = Node> = Output & {
+	node: T;
+	connection?: never;
+};
+export type ConnectedOutputWithDetails<T extends NodeBase = Node> = Output & {
+	node: T;
+	connection: Connection;
+};
+export type OutputWithDetails<T extends NodeBase = Node> =
+	| UnconnectedOutputWithDetails<T>
+	| ConnectedOutputWithDetails<T>;
diff --git a/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/outputs/use-categoried-outputs.ts b/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/outputs/use-categoried-outputs.ts
new file mode 100644
index 0000000000..9b02434362
--- /dev/null
+++ b/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/outputs/use-categoried-outputs.ts
@@ -0,0 +1,29 @@
+import {
+	isFileNode,
+	isGitHubNode,
+	isTextGenerationNode,
+	isTextNode,
+} from "@giselle-sdk/data-type";
+import { useMemo } from "react";
+import type { OutputWithDetails } from "./types";
+import { filterInputs } from "./utils";
+
+export function useCategoriedOutputs(inputs: OutputWithDetails[]) {
+	const generatedInputs = useMemo(
+		() => filterInputs(inputs, isTextGenerationNode),
+		[inputs],
+	);
+	const textInputs = useMemo(() => filterInputs(inputs, isTextNode), [inputs]);
+	const fileInputs = useMemo(() => filterInputs(inputs, isFileNode), [inputs]);
+	const githubInputs = useMemo(
+		() => filterInputs(inputs, isGitHubNode),
+		[inputs],
+	);
+
+	return {
+		generatedInputs,
+		textInputs,
+		fileInputs,
+		githubInputs,
+	};
+}
diff --git a/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/sources/use-connected-sources.ts b/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/outputs/use-connected-outputs.ts
similarity index 67%
rename from internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/sources/use-connected-sources.ts
rename to internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/outputs/use-connected-outputs.ts
index 4892916c72..8aace04bac 100644
--- a/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/sources/use-connected-sources.ts
+++ b/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/outputs/use-connected-outputs.ts
@@ -1,16 +1,18 @@
 import type { TextGenerationNode, VariableNode } from "@giselle-sdk/data-type";
 import { useWorkflowDesigner } from "giselle-sdk/react";
 import { useMemo } from "react";
-import type { ConnectedSource } from "./types";
+import type { ConnectedOutputWithDetails } from "./types";
 
-export function useConnectedSources(node: TextGenerationNode) {
+export function useConnectedOutputs(node: TextGenerationNode) {
 	const { data } = useWorkflowDesigner();
 	return useMemo(() => {
 		const connectionsToThisNode = data.connections.filter(
 			(connection) => connection.inputNode.id === node.id,
 		);
-		const connectedGeneratedSources: ConnectedSource<TextGenerationNode>[] = [];
-		const connectedVariableSources: ConnectedSource<VariableNode>[] = [];
+		const connectedGeneratedInputs: ConnectedOutputWithDetails<TextGenerationNode>[] =
+			[];
+		const connectedVariableInputs: ConnectedOutputWithDetails<VariableNode>[] =
+			[];
 		for (const connection of connectionsToThisNode) {
 			const node = data.nodes.find(
 				(node) => node.id === connection.outputNode.id,
@@ -29,8 +31,8 @@ export function useConnectedSources(node: TextGenerationNode) {
 				case "action":
 					switch (node.content.type) {
 						case "textGeneration":
-							connectedGeneratedSources.push({
-								output,
+							connectedGeneratedInputs.push({
+								...output,
 								node: node as TextGenerationNode,
 								connection,
 							});
@@ -38,8 +40,8 @@ export function useConnectedSources(node: TextGenerationNode) {
 					}
 					break;
 				case "variable":
-					connectedVariableSources.push({
-						output,
+					connectedVariableInputs.push({
+						...output,
 						node,
 						connection,
 					});
@@ -52,9 +54,9 @@ export function useConnectedSources(node: TextGenerationNode) {
 		}
 
 		return {
-			all: [...connectedGeneratedSources, ...connectedVariableSources],
-			generation: connectedGeneratedSources,
-			variable: connectedVariableSources,
+			all: [...connectedGeneratedInputs, ...connectedVariableInputs],
+			generation: connectedGeneratedInputs,
+			variable: connectedVariableInputs,
 		};
 	}, [node.id, data.connections, data.nodes]);
 }
diff --git a/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/outputs/utils.ts b/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/outputs/utils.ts
new file mode 100644
index 0000000000..896f7e80fc
--- /dev/null
+++ b/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/outputs/utils.ts
@@ -0,0 +1,19 @@
+import type { NodeBase } from "@giselle-sdk/data-type";
+import type { OutputWithDetails } from "./types";
+
+export function filterInputs<T extends NodeBase>(
+	inputs: OutputWithDetails[],
+	guardFn: (args: unknown) => args is T,
+): OutputWithDetails<T>[] {
+	const tmpInputs: OutputWithDetails<T>[] = [];
+	for (const input of inputs) {
+		if (!guardFn(input.node)) {
+			continue;
+		}
+		tmpInputs.push({
+			...input,
+			node: input.node,
+		});
+	}
+	return tmpInputs;
+}
diff --git a/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/prompt-panel.tsx b/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/prompt-panel.tsx
index dc5de2b5c0..7a299e96e0 100644
--- a/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/prompt-panel.tsx
+++ b/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/prompt-panel.tsx
@@ -11,9 +11,9 @@ import clsx from "clsx/lite";
 import { useWorkflowDesigner } from "giselle-sdk/react";
 import { AtSignIcon } from "lucide-react";
 import { DropdownMenu, Toolbar } from "radix-ui";
-import { type Source, useConnectedSources } from "./sources";
+import { type OutputWithDetails, useConnectedOutputs } from "./outputs";
 
-function getDefaultNodeName(source: Source): string {
+function getDefaultNodeName(source: OutputWithDetails): string {
 	if (isTextGenerationNode(source.node)) {
 		return source.node.content.llm.id;
 	}
@@ -26,7 +26,7 @@ export function PromptPanel({
 	node: TextGenerationNode;
 }) {
 	const { updateNodeDataContent } = useWorkflowDesigner();
-	const { all: connectedSources } = useConnectedSources(node);
+	const { all: connectedSources } = useConnectedOutputs(node);
 	return (
 		<TextEditor
 			value={node.content.prompt}
@@ -113,7 +113,7 @@ export function PromptPanel({
 												value={source.connection.id}
 											>
 												{source.node.name ?? getDefaultNodeName(source)}/{" "}
-												{source.output.label}
+												{source.label}
 											</DropdownMenu.RadioItem>
 										))}
 									</div>
diff --git a/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/sources/index.ts b/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/sources/index.ts
deleted file mode 100644
index 6f1d8834a7..0000000000
--- a/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/sources/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export * from "./types";
-export * from "./utils";
-export * from "./use-connected-sources";
-export * from "./use-source-categories";
diff --git a/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/sources/types.ts b/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/sources/types.ts
deleted file mode 100644
index 5ab5c2d1c1..0000000000
--- a/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/sources/types.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import type {
-	Connection,
-	Node,
-	NodeBase,
-	Output,
-} from "@giselle-sdk/data-type";
-
-export interface UnconnectedSource<T extends NodeBase = Node> {
-	output: Output;
-	node: T;
-	connection?: never;
-}
-export interface ConnectedSource<T extends NodeBase = Node> {
-	output: Output;
-	node: T;
-	connection: Connection;
-}
-export type Source<T extends NodeBase = Node> =
-	| UnconnectedSource<T>
-	| ConnectedSource<T>;
diff --git a/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/sources/use-source-categories.ts b/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/sources/use-source-categories.ts
deleted file mode 100644
index dff82a3fa4..0000000000
--- a/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/sources/use-source-categories.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import {
-	isFileNode,
-	isGitHubNode,
-	isTextGenerationNode,
-	isTextNode,
-} from "@giselle-sdk/data-type";
-import { useMemo } from "react";
-import type { Source } from "./types";
-import { filterSources } from "./utils";
-
-export function useSourceCategories(sources: Source[]) {
-	const generatedSources = useMemo(
-		() => filterSources(sources, isTextGenerationNode),
-		[sources],
-	);
-	const textSources = useMemo(
-		() => filterSources(sources, isTextNode),
-		[sources],
-	);
-	const fileSources = useMemo(
-		() => filterSources(sources, isFileNode),
-		[sources],
-	);
-	const githubSources = useMemo(
-		() => filterSources(sources, isGitHubNode),
-		[sources],
-	);
-
-	return {
-		generatedSources,
-		textSources,
-		fileSources,
-		githubSources,
-	};
-}
diff --git a/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/sources/utils.ts b/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/sources/utils.ts
deleted file mode 100644
index 50985fc73a..0000000000
--- a/internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/sources/utils.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import type { NodeBase } from "@giselle-sdk/data-type";
-import type { Source } from "./types";
-
-export function filterSources<T extends NodeBase>(
-	sources: Source[],
-	guardFn: (args: unknown) => args is T,
-): Source<T>[] {
-	const tmpSources: Source<T>[] = [];
-	for (const source of sources) {
-		if (!guardFn(source.node)) {
-			continue;
-		}
-		tmpSources.push({
-			...source,
-			node: source.node,
-		});
-	}
-	return tmpSources;
-}
diff --git a/packages/giselle-engine/src/core/generations/utils.ts b/packages/giselle-engine/src/core/generations/utils.ts
index 0e7da7d739..7c4314e0fa 100644
--- a/packages/giselle-engine/src/core/generations/utils.ts
+++ b/packages/giselle-engine/src/core/generations/utils.ts
@@ -164,6 +164,7 @@ async function buildGenerationMessageForTextGeneration(
 				{
 					role: "user",
 					content: [
+						...attachedFiles,
 						{
 							type: "text",
 							text: userMessage,
diff --git a/packages/workflow-designer/src/is-supported-connection.ts b/packages/workflow-designer/src/is-supported-connection.ts
index c8c3e7bb38..0a29adfa97 100644
--- a/packages/workflow-designer/src/is-supported-connection.ts
+++ b/packages/workflow-designer/src/is-supported-connection.ts
@@ -42,7 +42,10 @@ export function isSupportedConnection(
 			};
 		}
 
-		if (inputNode.content.llm.provider === "openai") {
+		if (
+			inputNode.content.llm.provider === "openai" &&
+			outputNode.content.category !== "image"
+		) {
 			return {
 				canConnect: false,
 				message: "File node is not supported as an input for OpenAI",


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.


<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**packages/workflow-designer/src/is-supported-connection.ts:44**
* The error message still refers to 'File node is not supported as an input for OpenAI', which might be misleading now that image nodes are allowed. Consider updating the message to clearly reflect that only image nodes are supported.
```
if (inputNode.content.llm.provider === "openai" && outputNode.content.category !== "image") {
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T06:08:50Z
user: login: shige
body: Thank you! 🖼️ 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-16T06:30:31Z

Review comments:
diff_hunk: @@ -0,0 +1,275 @@
+import { OutputId, type TextGenerationNode } from "@giselle-sdk/data-type";
+import clsx from "clsx/lite";
+import { useWorkflowDesigner } from "giselle-sdk/react";
+import { CheckIcon, TrashIcon } from "lucide-react";
+import { Popover, ToggleGroup } from "radix-ui";
+import {
+	type ComponentProps,
+	type ReactNode,
+	useCallback,
+	useState,
+} from "react";
+import type { OutputWithDetails } from "./types";
+import { useCategoriedOutputs } from "./use-categoried-outputs";
+
+function OutputToggleItem({
+	input,
+	disabled = false,
+}: { input: OutputWithDetails; disabled?: boolean }) {
+	const getDisplayName = () => {
+		if ("content" in input.node && "llm" in input.node.content) {
+			return input.node.name ?? input.node.content.llm.id;
+		}
+		return input.node.name ?? "Source";
+	};
+
+	return (
+		<ToggleGroup.Item
+			key={input.id}
+			className={clsx(
+				"group flex p-[8px] justify-between rounded-[8px] hover:bg-primary-900/50 transition-colors cursor-pointer",
+				"text-white-400",
+				"data-[disabled]:text-white-850/30 data-[disabled]:pointer-events-none",
+			)}
+			value={input.id}
+			disabled={disabled}
+		>
+			<p className="text-[12px] truncate">
+				{getDisplayName()} / {input.label}
+			</p>
+			<CheckIcon className="w-[16px] h-[16px] hidden group-data-[state=on]:block" />
+			<div
+				className={clsx(
+					"px-[10px] py-[4px] flex items-center justify-center rounded-[30px]",
+					"bg-black-200/20 text-black-200/20 text-[10px]",
+					"hidden group-data-[disabled]:block",
+				)}
+			>
+				Unsupported
+			</div>
+		</ToggleGroup.Item>
+	);
+}
+
+export function SelectOutputPopover({
+	node,
+	outputs,
+	onValueChange,
+	contentProps,
+}: {
+	node: TextGenerationNode;
+	outputs: OutputWithDetails[];
+	onValueChange?: (value: OutputId[]) => void;
+	contentProps?: Omit<
+		ComponentProps<typeof Popover.PopoverContent>,
+		"className"
+	>;
+}) {
+	const [selectedOutputIds, setSelectedOutputIds] = useState<OutputId[]>([]);
+	const { generatedInputs, textInputs, fileInputs, githubInputs } =
+		useCategoriedOutputs(outputs);
+	const { isSupportedConnection } = useWorkflowDesigner();
+	const isSupported = useCallback(
+		(input: OutputWithDetails) => {
+			const { canConnect } = isSupportedConnection(input.node, node);
+			return canConnect;
+		},
+		[isSupportedConnection, node],
+	);
+
+	return (
+		<Popover.Root
+			onOpenChange={(open) => {
+				if (open) {
+					setSelectedOutputIds(
+						outputs
+							.filter((input) => input.connection !== undefined)
+							.map((input) => input.id),
+					);
+				}
+			}}
+		>
+			<Popover.Trigger
+				className={clsx(
+					"flex items-center cursor-pointer p-[10px] rounded-[8px]",
+					"border border-transparent hover:border-white-800",
+					"text-[12px] font-[700] text-white-800",
+					"transition-colors",
+				)}
+			>
+				Select Sources
+			</Popover.Trigger>
+			<Popover.Anchor />
+			<Popover.Portal>
+				<Popover.Content
+					className={clsx(
+						"relative w-[300px] py-[8px]",
+						"rounded-[8px] border-[1px] bg-black-900/60 backdrop-blur-[8px]",
+						"shadow-[-2px_-1px_0px_0px_rgba(0,0,0,0.1),1px_1px_8px_0px_rgba(0,0,0,0.25)]",
+					)}
+					{...contentProps}
+				>
+					<div
+						className={clsx(
+							"absolute z-0 rounded-[8px] inset-0 border-[1px] mask-fill bg-gradient-to-br bg-origin-border bg-clip-boarder border-transparent",
+							"from-[hsl(232,_36%,_72%)]/40 to-[hsl(218,_58%,_21%)]/90",
+						)}
+					/>
+					<ToggleGroup.Root
+						type="multiple"
+						className="relative max-h-[300px] flex flex-col"
+						value={selectedOutputIds}
+						onValueChange={(unsafeValue) => {
+							const safeValue = unsafeValue
+								.map((value) => {
+									const parse = OutputId.safeParse(value);
+									if (parse.success) {
+										return parse.data;
+									}
+									return null;
+								})
+								.filter((id) => id !== null);
+							setSelectedOutputIds(safeValue);
+						}}
+					>
+						<div className="flex px-[16px] text-white-900">
+							Select Sources From
+						</div>
+						<div className="flex flex-col py-[4px]">
+							<div className="border-t border-black-300/20" />
+						</div>
+						<div className="grow flex flex-col pb-[8px] gap-[8px] overflow-y-auto min-h-0">
+							{generatedInputs.length > 0 && (
+								<div className="flex flex-col px-[8px]">
+									<p className="py-[4px] px-[8px] text-black-400 text-[10px] font-[700]">
+										Generated Content
+									</p>
+									{generatedInputs.map((generatedInput) => (
+										<OutputToggleItem
+											key={generatedInput.id}
+											input={generatedInput}
+											disabled={!isSupported(generatedInput)}
+										/>
+									))}
+								</div>
+							)}
+							{textInputs.length > 0 && (
+								<div className="flex flex-col px-[8px]">
+									<p className="py-[4px] px-[8px] text-black-400 text-[10px] font-[700]">
+										Text
+									</p>
+									{textInputs.map((textInput) => (
+										<OutputToggleItem
+											key={textInput.id}
+											input={textInput}
+											disabled={!isSupported(textInput)}
+										/>
+									))}
+								</div>
+							)}
+
+							{fileInputs.length > 0 && (
+								<div className="flex flex-col px-[8px]">
+									<p className="py-[4px] px-[8px] text-black-400 text-[10px] font-[700]">
+										File
+									</p>
+									{fileInputs.map((fileInput) => (
+										<OutputToggleItem
+											key={fileInput.id}
+											input={fileInput}
+											disabled={!isSupported(fileInput)}
+										/>
+									))}
+								</div>
+							)}
+							{githubInputs.length > 0 && (
+								<div className="flex flex-col px-[8px]">
+									<p className="py-[4px] px-[8px] text-black-400 text-[10px] font-[700]">
+										GitHub
+									</p>
+									{githubInputs.map((githubInput) => (
+										<OutputToggleItem
+											key={githubInput.id}
+											input={githubInput}
+											disabled={!isSupported(githubInput)}
+										/>
+									))}
+								</div>
+							)}
+						</div>
+						<div className="flex flex-col py-[4px]">
+							<div className="border-t border-black-300/20" />
+						</div>
+						<div className="flex px-[16px] py-[4px] gap-[8px]">
+							<Popover.Close
+								onClick={() => {
+									onValueChange?.(selectedOutputIds);
+								}}
+								className="h-[32px] w-full flex justify-center items-center bg-white-900 text-black-900 rounded-[8px] cursor-pointer text-[12px]"
+							>
+								Update
+							</Popover.Close>
+						</div>
+					</ToggleGroup.Root>
+				</Popover.Content>
+			</Popover.Portal>
+		</Popover.Root>
+	);
+}
+
+export function ConnectedOutputListRoot({
+	title,
+	children,
+}: {
+	title: string;
+	children: ReactNode;
+}) {
+	return (
+		<div className="flex flex-col gap-[8px]">
+			<p className="text-[14px]">{title}</p>
+			{children}
+		</div>
+	);
+}
+export function ConnectedOutputListItem({
+	icon,
+	title,
+	subtitle,
+	onRemove,
+}: {
+	icon: ReactNode;
+	title: string;
+	subtitle: string;
+	onRemove: () => void;
+}) {
+	return (
+		<div
+			className={clsx(
+				"group flex items-center",
+				"border border-white-900/20 rounded-[8px] h-[60px]",
+			)}
+		>
+			<div className="w-[60px] flex items-center justify-center">{icon}</div>
+			<div className="w-[1px] h-full border-l border-white-900/20" />
+			<div className="px-[16px] flex-1 flex items-center justify-between">
+				<div className="flex flex-col gap-[4px]">
+					<p className="text=[16px]">{title}</p>
path: internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/outputs/components.tsx
user: login: Copilot
body: It appears there is a typographical error in the CSS class name; 'text=[16px]' should likely be 'text-[16px]'.
```suggestion
					<p className="text-[16px]">{title}</p>
```
created_at: 2025-04-16T06:08:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,275 @@
+import { OutputId, type TextGenerationNode } from "@giselle-sdk/data-type";
+import clsx from "clsx/lite";
+import { useWorkflowDesigner } from "giselle-sdk/react";
+import { CheckIcon, TrashIcon } from "lucide-react";
+import { Popover, ToggleGroup } from "radix-ui";
+import {
+	type ComponentProps,
+	type ReactNode,
+	useCallback,
+	useState,
+} from "react";
+import type { OutputWithDetails } from "./types";
+import { useCategoriedOutputs } from "./use-categoried-outputs";
+
+function OutputToggleItem({
+	input,
+	disabled = false,
+}: { input: OutputWithDetails; disabled?: boolean }) {
+	const getDisplayName = () => {
+		if ("content" in input.node && "llm" in input.node.content) {
+			return input.node.name ?? input.node.content.llm.id;
+		}
+		return input.node.name ?? "Source";
+	};
+
+	return (
+		<ToggleGroup.Item
+			key={input.id}
+			className={clsx(
+				"group flex p-[8px] justify-between rounded-[8px] hover:bg-primary-900/50 transition-colors cursor-pointer",
+				"text-white-400",
+				"data-[disabled]:text-white-850/30 data-[disabled]:pointer-events-none",
+			)}
+			value={input.id}
+			disabled={disabled}
+		>
+			<p className="text-[12px] truncate">
+				{getDisplayName()} / {input.label}
+			</p>
+			<CheckIcon className="w-[16px] h-[16px] hidden group-data-[state=on]:block" />
+			<div
+				className={clsx(
+					"px-[10px] py-[4px] flex items-center justify-center rounded-[30px]",
+					"bg-black-200/20 text-black-200/20 text-[10px]",
+					"hidden group-data-[disabled]:block",
+				)}
+			>
+				Unsupported
+			</div>
+		</ToggleGroup.Item>
+	);
+}
+
+export function SelectOutputPopover({
+	node,
+	outputs,
+	onValueChange,
+	contentProps,
+}: {
+	node: TextGenerationNode;
+	outputs: OutputWithDetails[];
+	onValueChange?: (value: OutputId[]) => void;
+	contentProps?: Omit<
+		ComponentProps<typeof Popover.PopoverContent>,
+		"className"
+	>;
+}) {
+	const [selectedOutputIds, setSelectedOutputIds] = useState<OutputId[]>([]);
+	const { generatedInputs, textInputs, fileInputs, githubInputs } =
+		useCategoriedOutputs(outputs);
+	const { isSupportedConnection } = useWorkflowDesigner();
+	const isSupported = useCallback(
+		(input: OutputWithDetails) => {
+			const { canConnect } = isSupportedConnection(input.node, node);
+			return canConnect;
+		},
+		[isSupportedConnection, node],
+	);
+
+	return (
+		<Popover.Root
+			onOpenChange={(open) => {
+				if (open) {
+					setSelectedOutputIds(
+						outputs
+							.filter((input) => input.connection !== undefined)
+							.map((input) => input.id),
+					);
+				}
+			}}
+		>
+			<Popover.Trigger
+				className={clsx(
+					"flex items-center cursor-pointer p-[10px] rounded-[8px]",
+					"border border-transparent hover:border-white-800",
+					"text-[12px] font-[700] text-white-800",
+					"transition-colors",
+				)}
+			>
+				Select Sources
+			</Popover.Trigger>
+			<Popover.Anchor />
+			<Popover.Portal>
+				<Popover.Content
+					className={clsx(
+						"relative w-[300px] py-[8px]",
+						"rounded-[8px] border-[1px] bg-black-900/60 backdrop-blur-[8px]",
+						"shadow-[-2px_-1px_0px_0px_rgba(0,0,0,0.1),1px_1px_8px_0px_rgba(0,0,0,0.25)]",
+					)}
+					{...contentProps}
+				>
+					<div
+						className={clsx(
+							"absolute z-0 rounded-[8px] inset-0 border-[1px] mask-fill bg-gradient-to-br bg-origin-border bg-clip-boarder border-transparent",
+							"from-[hsl(232,_36%,_72%)]/40 to-[hsl(218,_58%,_21%)]/90",
+						)}
+					/>
+					<ToggleGroup.Root
+						type="multiple"
+						className="relative max-h-[300px] flex flex-col"
+						value={selectedOutputIds}
+						onValueChange={(unsafeValue) => {
+							const safeValue = unsafeValue
+								.map((value) => {
+									const parse = OutputId.safeParse(value);
+									if (parse.success) {
+										return parse.data;
+									}
+									return null;
+								})
+								.filter((id) => id !== null);
+							setSelectedOutputIds(safeValue);
+						}}
+					>
+						<div className="flex px-[16px] text-white-900">
+							Select Sources From
+						</div>
+						<div className="flex flex-col py-[4px]">
+							<div className="border-t border-black-300/20" />
+						</div>
+						<div className="grow flex flex-col pb-[8px] gap-[8px] overflow-y-auto min-h-0">
+							{generatedInputs.length > 0 && (
+								<div className="flex flex-col px-[8px]">
+									<p className="py-[4px] px-[8px] text-black-400 text-[10px] font-[700]">
+										Generated Content
+									</p>
+									{generatedInputs.map((generatedInput) => (
+										<OutputToggleItem
+											key={generatedInput.id}
+											input={generatedInput}
+											disabled={!isSupported(generatedInput)}
+										/>
+									))}
+								</div>
+							)}
+							{textInputs.length > 0 && (
+								<div className="flex flex-col px-[8px]">
+									<p className="py-[4px] px-[8px] text-black-400 text-[10px] font-[700]">
+										Text
+									</p>
+									{textInputs.map((textInput) => (
+										<OutputToggleItem
+											key={textInput.id}
+											input={textInput}
+											disabled={!isSupported(textInput)}
+										/>
+									))}
+								</div>
+							)}
+
+							{fileInputs.length > 0 && (
+								<div className="flex flex-col px-[8px]">
+									<p className="py-[4px] px-[8px] text-black-400 text-[10px] font-[700]">
+										File
+									</p>
+									{fileInputs.map((fileInput) => (
+										<OutputToggleItem
+											key={fileInput.id}
+											input={fileInput}
+											disabled={!isSupported(fileInput)}
+										/>
+									))}
+								</div>
+							)}
+							{githubInputs.length > 0 && (
+								<div className="flex flex-col px-[8px]">
+									<p className="py-[4px] px-[8px] text-black-400 text-[10px] font-[700]">
+										GitHub
+									</p>
+									{githubInputs.map((githubInput) => (
+										<OutputToggleItem
+											key={githubInput.id}
+											input={githubInput}
+											disabled={!isSupported(githubInput)}
+										/>
+									))}
+								</div>
+							)}
+						</div>
+						<div className="flex flex-col py-[4px]">
+							<div className="border-t border-black-300/20" />
+						</div>
+						<div className="flex px-[16px] py-[4px] gap-[8px]">
+							<Popover.Close
+								onClick={() => {
+									onValueChange?.(selectedOutputIds);
+								}}
+								className="h-[32px] w-full flex justify-center items-center bg-white-900 text-black-900 rounded-[8px] cursor-pointer text-[12px]"
+							>
+								Update
+							</Popover.Close>
+						</div>
+					</ToggleGroup.Root>
+				</Popover.Content>
+			</Popover.Portal>
+		</Popover.Root>
+	);
+}
+
+export function ConnectedOutputListRoot({
+	title,
+	children,
+}: {
+	title: string;
+	children: ReactNode;
+}) {
+	return (
+		<div className="flex flex-col gap-[8px]">
+			<p className="text-[14px]">{title}</p>
+			{children}
+		</div>
+	);
+}
+export function ConnectedOutputListItem({
+	icon,
+	title,
+	subtitle,
+	onRemove,
+}: {
+	icon: ReactNode;
+	title: string;
+	subtitle: string;
+	onRemove: () => void;
+}) {
+	return (
+		<div
+			className={clsx(
+				"group flex items-center",
+				"border border-white-900/20 rounded-[8px] h-[60px]",
+			)}
+		>
+			<div className="w-[60px] flex items-center justify-center">{icon}</div>
+			<div className="w-[1px] h-full border-l border-white-900/20" />
+			<div className="px-[16px] flex-1 flex items-center justify-between">
+				<div className="flex flex-col gap-[4px]">
+					<p className="text=[16px]">{title}</p>
path: internal-packages/workflow-designer-ui/src/editor/properties-panel/text-generation-node-properties-panel/outputs/components.tsx
user: login: shige
body: @toyamarinyon If it seems necessary, I'd like to ask you to commit.
created_at: 2025-04-16T06:23:08Z
author_association: MEMBER

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-04-16T04:32:25Z
body: ###  ⚠️  No Changeset found

Latest commit: f188c9e2fc19c7258e5f8a9a46c552cc0f86a45d

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**



[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/giselles-ai/giselle/new/refactor-workflow-source-to-input-output?filename=.changeset/seven-kiwis-talk.md&value=---%0A%22%40fake-scope%2Ffake-pkg%22%3A%20patch%0A---%0A%0AAdded%20support%20for%20connecting%20image%20nodes%20to%20Text%20Generation%20nodes%20using%20OpenAI%20models%0A)

<details><summary>💥 An error occurred when fetching the changed packages and changesets in this PR</summary>

```
Some errors occurred when validating the changesets config:
The package "playground" depends on the ignored package "giselle-sdk", but "playground" is not being ignored. Please add "playground" to the `ignore` option.
The package "studio.giselles.ai" depends on the ignored package "giselle-sdk", but "studio.giselles.ai" is not being ignored. Please add "studio.giselles.ai" to the `ignore` option.
The package "@giselle-internal/workflow-designer-ui" depends on the ignored package "giselle-sdk", but "@giselle-internal/workflow-designer-ui" is not being ignored. Please add "@giselle-internal/workflow-designer-ui" to the `ignore` option.
```

</details>

author_association: NONE
user: login: vercel[bot]
created_at: 2025-04-16T04:32:27Z
body: [vc]: #zPZ0F8LjsKYtmeq3cFPCKKbZvMu3QGqtE5OJfvYef2g=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJnaXNlbGxlIiwicm9vdERpcmVjdG9yeSI6ImFwcHMvc3R1ZGlvLmdpc2VsbGVzLmFpIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6Imdpc2VsbGUtZ2l0LXJlZmFjdG9yLXdvcmtmbG93LXNvdXJjZS10by1pbnB1dC1vdXRwdXQtcjA2LWVkZ2UudmVyY2VsLmFwcCJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vcjA2LWVkZ2UvZ2lzZWxsZS9FTHgxMnBVY0xGSnJtUXdib3l1UmU1UTJEMlJBIiwicHJldmlld1VybCI6Imdpc2VsbGUtZ2l0LXJlZmFjdG9yLXdvcmtmbG93LXNvdXJjZS10by1pbnB1dC1vdXRwdXQtcjA2LWVkZ2UudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCJ9LHsibmFtZSI6Imdpc2VsbGUtcGxheWdyb3VuZCIsInJvb3REaXJlY3RvcnkiOiJhcHBzL3BsYXlncm91bmQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiZ2lzZWxsZS1wbGF5Z3JvdW5kLWdpdC1yZWZhY3Rvci13b3JrZmxvdy1zb3VyY2UtNzM3ZDg3LXIwNi1lZGdlLnZlcmNlbC5hcHAifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3IwNi1lZGdlL2dpc2VsbGUtcGxheWdyb3VuZC9DUEtrTnllRG41RVlEVk03YXZkRE5DTTg0TGJqIiwicHJldmlld1VybCI6Imdpc2VsbGUtcGxheWdyb3VuZC1naXQtcmVmYWN0b3Itd29ya2Zsb3ctc291cmNlLTczN2Q4Ny1yMDYtZWRnZS52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIn1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **giselle** | ✅ Ready ([Inspect](https://vercel.com/r06-edge/giselle/ELx12pUcLFJrmQwboyuRe5Q2D2RA)) | [Visit Preview](https://giselle-git-refactor-workflow-source-to-input-output-r06-edge.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/giselle-git-refactor-workflow-source-to-input-output-r06-edge.vercel.app?via=pr-comment-feedback-link) | Apr 16, 2025 7:09am |
| **giselle-playground** | ✅ Ready ([Inspect](https://vercel.com/r06-edge/giselle-playground/CPKkNyeDn5EYDVM7avdDNCM84Lbj)) | [Visit Preview](https://giselle-playground-git-refactor-workflow-source-737d87-r06-edge.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/giselle-playground-git-refactor-workflow-source-737d87-r06-edge.vercel.app?via=pr-comment-feedback-link) | Apr 16, 2025 7:09am |



author_association: NONE