From db72ac1c705b4eac0df1e2d8c55393a82cd947ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=AF=BC=EC=84=9D?= Date: Wed, 15 Apr 2026 13:13:55 +0900 Subject: [PATCH 1/2] feat: add 3-tier field classification for chat mode input Add Required/Normal/NotRequired field tiers for chat mode schemas. Fields are classified by x-field-display override and JSON Schema required array, with NotRequired fields shown in Advanced Input only. --- .../docs/reference/configuration.ko.mdx | 67 ++++++++++++ .../content/docs/reference/configuration.mdx | 67 ++++++++++++ .../schema-ui/InlineFieldsSection.tsx | 102 ++++++++++++++++++ .../schema-ui/SchemaFieldsSection.tsx | 15 ++- .../components/schema-ui/UnifiedInputArea.tsx | 72 +++++-------- .../src/features/chat/hooks/useSchemaUI.ts | 2 + frontend/src/lib/utils/schema.ts | 95 ++++++++++++++-- frontend/src/types/schema-ui.ts | 19 ++++ 8 files changed, 379 insertions(+), 60 deletions(-) create mode 100644 frontend/src/features/chat/components/schema-ui/InlineFieldsSection.tsx diff --git a/docs-site/content/docs/reference/configuration.ko.mdx b/docs-site/content/docs/reference/configuration.ko.mdx index 0242a67..417f11d 100644 --- a/docs-site/content/docs/reference/configuration.ko.mdx +++ b/docs-site/content/docs/reference/configuration.ko.mdx @@ -94,6 +94,73 @@ description: 모든 설정 옵션에 대한 완전한 레퍼런스 |---|---|---|---| | `SERVER_ACTION_BODY_SIZE_LIMIT` | - | `10mb` | 서버 액션의 최대 본문 크기 | +## 입력 스키마 필드 표시 (Chat 모드) + +**Chat 모드** (state에 `messages` 필드가 있는 경우)에서 추가 필드는 UI 표시 방식에 따라 3단계로 분류됩니다: + +| 분류 | UI 위치 | Submit 시 필수 | 빨간 표시 | +|---|---|---|---| +| **Required** | Input 박스 위 | O | O | +| **Normal (일반)** | Input 박스 위 | X | X | +| **NotRequired** | 고급 입력 (접기/펼치기) | X | X | + +### 분류 규칙 + +| 조건 | 분류 | +|---|---| +| `x-field-display: "required"` 설정됨 | **Required** | +| `x-field-display: "inline"` 설정됨 | **Normal** | +| `x-field-display: "advanced"` 설정됨 | **NotRequired** | +| `required` 배열에 포함 (기본값) | **Normal** | +| `required` 배열에 미포함 (Python `NotRequired`) | **NotRequired** | + +### 백엔드 예시 + +**TypedDict:** + +```python +from typing import Annotated, NotRequired, TypedDict +from pydantic import Field + +class InputState(TypedDict): + messages: Annotated[list, operator.add] + + # Required — 빨간 표시, 값 필수 + query: Annotated[str, Field(json_schema_extra={"x-field-display": "required"})] + + # Normal (일반) — Input 위에 표시, 선택 사항 + context: str + + # NotRequired — 고급 입력에만 표시 + temperature: NotRequired[float] +``` + +**Pydantic BaseModel:** + +```python +from pydantic import BaseModel, Field + +class InputState(BaseModel): + messages: list + + # Required — 빨간 표시, 값 필수 + query: str = Field(json_schema_extra={"x-field-display": "required"}) + + # Normal (일반) — Input 위에 표시, 선택 사항 + context: str = Field(default="") + + # NotRequired — 고급 입력에만 표시 + temperature: float = 0.7 +``` + +> **참고:** TypedDict에서는 모든 필드가 기본적으로 `required` 배열에 포함됩니다. 필드를 고급 입력으로 이동하려면 `NotRequired[T]`를 사용하고, 명시적으로 제어하려면 `x-field-display`를 사용하세요. + +### Form 모드 + +**Form 모드** (`messages` 필드가 없는 경우)에서는 `x-field-display`가 사용되지 않습니다. 표준 JSON Schema 분류를 따릅니다: +- `required` 배열에 포함된 필드 → 필수 필드로 표시 +- 그 외 필드 → 고급 입력에 표시 + ## 사이트 설정 (site.ts) `src/configs/site.ts` 파일은 앱의 외형과 동작을 제어합니다. diff --git a/docs-site/content/docs/reference/configuration.mdx b/docs-site/content/docs/reference/configuration.mdx index 2357666..1653a76 100644 --- a/docs-site/content/docs/reference/configuration.mdx +++ b/docs-site/content/docs/reference/configuration.mdx @@ -94,6 +94,73 @@ All environment variables are configured in the `.env` file at the project root. |---|---|---|---| | `SERVER_ACTION_BODY_SIZE_LIMIT` | No | `10mb` | Maximum body size for server actions | +## Input Schema Field Display (Chat Mode) + +When using **chat mode** (graph state with `messages` field), additional fields are classified into three tiers based on how they appear in the UI: + +| Tier | UI Position | Required to Submit | Red Indicator | +|---|---|---|---| +| **Required** | Above input box | Yes | Yes | +| **Normal** | Above input box | No | No | +| **NotRequired** | Advanced Input (collapsible) | No | No | + +### Classification Rules + +| Condition | Tier | +|---|---| +| `x-field-display: "required"` set | **Required** | +| `x-field-display: "inline"` set | **Normal** | +| `x-field-display: "advanced"` set | **NotRequired** | +| In `required` array (default) | **Normal** | +| Not in `required` array (`NotRequired` in Python) | **NotRequired** | + +### Backend Examples + +**TypedDict:** + +```python +from typing import Annotated, NotRequired, TypedDict +from pydantic import Field + +class InputState(TypedDict): + messages: Annotated[list, operator.add] + + # Required — red indicator, must fill to submit + query: Annotated[str, Field(json_schema_extra={"x-field-display": "required"})] + + # Normal — shown above input, optional + context: str + + # NotRequired — shown in Advanced Input only + temperature: NotRequired[float] +``` + +**Pydantic BaseModel:** + +```python +from pydantic import BaseModel, Field + +class InputState(BaseModel): + messages: list + + # Required — red indicator, must fill to submit + query: str = Field(json_schema_extra={"x-field-display": "required"}) + + # Normal — shown above input, optional + context: str = Field(default="") + + # NotRequired — shown in Advanced Input only + temperature: float = 0.7 +``` + +> **Note:** In TypedDict, all fields are in the `required` array by default. Use `NotRequired[T]` to move a field to the Advanced Input section, or `x-field-display` for explicit control. + +### Form Mode + +In **form mode** (no `messages` field), `x-field-display` is not used. Fields follow standard JSON Schema classification: +- Fields in `required` array → shown as required fields +- Other fields → shown in Advanced Input + ## Site Configuration (site.ts) The `src/configs/site.ts` file controls the app's appearance and behavior. diff --git a/frontend/src/features/chat/components/schema-ui/InlineFieldsSection.tsx b/frontend/src/features/chat/components/schema-ui/InlineFieldsSection.tsx new file mode 100644 index 0000000..9283ca0 --- /dev/null +++ b/frontend/src/features/chat/components/schema-ui/InlineFieldsSection.tsx @@ -0,0 +1,102 @@ +/** + * Inline Fields Section Component + * Renders Required + Normal fields above the textarea in chat mode. + * Required fields must be filled for submit; Normal fields are optional. + */ + +import React from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { SchemaField } from "./SchemaField"; +import type { SchemaFieldConfig, FieldValue, JSONSchema } from "@/types/schema-ui"; + +interface InlineFieldsSectionProps { + requiredFields: SchemaFieldConfig[]; + normalFields: SchemaFieldConfig[]; + rootSchema: JSONSchema; + formState: Record; + displayState: Record; + onFieldChange: (name: string, value: FieldValue) => void; + onDisplayValueChange: (name: string, value: FieldValue) => void; + disabled?: boolean; + fileUploadMode?: "base64" | "url"; +} + +export function InlineFieldsSection({ + requiredFields, + normalFields, + rootSchema, + formState, + displayState, + onFieldChange, + onDisplayValueChange, + disabled = false, + fileUploadMode, +}: InlineFieldsSectionProps) { + const hasRequired = requiredFields.length > 0; + const hasNormal = normalFields.length > 0; + + if (!hasRequired && !hasNormal) { + return null; + } + + return ( + + + {/* Required fields */} + {hasRequired && ( +
+ {requiredFields.map((field) => ( + onFieldChange(field.name, value)} + onDisplayValueChange={(value) => + onDisplayValueChange(field.name, value) + } + disabled={disabled} + compact + fileUploadMode={fileUploadMode} + /> + ))} +
+ )} + + {/* Separator between required and normal sections */} + {hasRequired && hasNormal && ( +
+ )} + + {/* Normal (optional) fields */} + {hasNormal && ( +
+ {normalFields.map((field) => ( + onFieldChange(field.name, value)} + onDisplayValueChange={(value) => + onDisplayValueChange(field.name, value) + } + disabled={disabled} + compact + fileUploadMode={fileUploadMode} + /> + ))} +
+ )} + + + ); +} diff --git a/frontend/src/features/chat/components/schema-ui/SchemaFieldsSection.tsx b/frontend/src/features/chat/components/schema-ui/SchemaFieldsSection.tsx index e94af60..430a782 100644 --- a/frontend/src/features/chat/components/schema-ui/SchemaFieldsSection.tsx +++ b/frontend/src/features/chat/components/schema-ui/SchemaFieldsSection.tsx @@ -11,9 +11,12 @@ import { ChevronDown, Sparkles } from "lucide-react"; import { cn } from "@/lib/utils"; import { SchemaField } from "./SchemaField"; import type { UseSchemaUIReturn } from "@/features/chat/hooks/useSchemaUI"; +import type { SchemaFieldConfig } from "@/types/schema-ui"; interface SchemaFieldsSectionProps { schemaUI: UseSchemaUIReturn; + /** Override which fields to display. When provided, used instead of optionalFields from schemaUI. */ + fields?: SchemaFieldConfig[]; disabled?: boolean; className?: string; fileUploadMode?: "base64" | "url"; @@ -21,6 +24,7 @@ interface SchemaFieldsSectionProps { export function SchemaFieldsSection({ schemaUI, + fields: fieldsProp, disabled = false, className, fileUploadMode, @@ -38,8 +42,11 @@ export function SchemaFieldsSection({ const { optionalFields, rawSchema } = parsedSchema; - // Don't render if no optional fields - if (!optionalFields.length || !rawSchema) { + // Use explicit fields prop if provided, otherwise fall back to optionalFields + const displayFields = fieldsProp ?? optionalFields; + + // Don't render if no fields to display + if (!displayFields.length || !rawSchema) { return null; } @@ -67,7 +74,7 @@ export function SchemaFieldsSection({ - ({optionalFields.length}) + ({displayFields.length}) @@ -81,7 +88,7 @@ export function SchemaFieldsSection({ className="overflow-hidden" >
- {optionalFields.map((field) => ( + {displayFields.map((field) => ( - {/* 공통: SchemaFieldsSection - 상단, 고급 입력 (optional fields) */} + {/* 공통: SchemaFieldsSection - 상단, 고급 입력 */} + {/* Form mode: all optional fields / Chat mode: notRequired fields only */} {enableAdvancedInput ? ( @@ -309,52 +311,32 @@ export function UnifiedInputArea({ ) ) : ( - /* Chat mode: textarea + file upload + submit */ + /* Chat mode: inline fields + textarea + file upload + submit */ (() => { - // Find required file fields in chat mode schema - const requiredFileFields = rawSchema - ? requiredFields.filter((f) => { - const ft = getFieldType( - f.schema, - rawSchema, - ) as SchemaFieldType; - const itemSchema = - ft === "array" ? getArrayItemSchema(f, rawSchema) : null; - const it = itemSchema - ? (getFieldType(itemSchema, rawSchema) as SchemaFieldType) - : undefined; - return isFileField(f.name, ft, it); - }) - : []; - - // Check if all required file fields have values - const fileFieldsValid = requiredFileFields.every((f) => { + // Check if all required fields have values (all types, not just file) + const requiredFieldsValid = requiredFields.every((f) => { const v = formState[f.name]; + if (v === null || v === undefined) return false; + if (typeof v === "string") return v.trim() !== ""; if (Array.isArray(v)) return v.length > 0; - return !!v; + return true; }); return ( <> - {/* Required file fields for chat mode schemas */} - {requiredFileFields.length > 0 && rawSchema && ( -
- {requiredFileFields.map((field) => ( - setFieldValue(field.name, value)} - onDisplayValueChange={(value) => - setFieldDisplayValue(field.name, value) - } - disabled={isLoading} - fileUploadMode={fileUploadMode} - /> - ))} -
+ {/* Required + Normal fields above textarea */} + {rawSchema && ( + )} ).length === 0 + ) + return false; + return true; +} + +/** + * Parse input schema and categorize fields. + * + * Form mode: 2-tier (requiredFields + optionalFields). + * Chat mode: 3-tier using `required` array + x-field-display override: + * - Required: x-field-display: "required" (red mark, must-fill) + * - Normal: in `required` array (inline, optional, no red mark) + * - NotRequired: NOT in `required` array, i.e. Python NotRequired (advanced only) */ export function parseInputSchema( inputSchema: JSONSchema | null, @@ -221,6 +257,8 @@ export function parseInputSchema( uiMode, requiredFields: [], optionalFields: [], + normalFields: [], + notRequiredFields: [], hasMessages, rawSchema: inputSchema, }; @@ -229,28 +267,61 @@ export function parseInputSchema( const requiredSet = new Set(inputSchema.required || []); const requiredFields: SchemaFieldConfig[] = []; const optionalFields: SchemaFieldConfig[] = []; + const normalFields: SchemaFieldConfig[] = []; + const notRequiredFields: SchemaFieldConfig[] = []; // Fields to exclude from the form (handled separately) const excludedFields = new Set(["messages", "ui"]); for (const [name, schema] of Object.entries(inputSchema.properties)) { - // Skip excluded fields if (excludedFields.has(name)) { continue; } const resolvedSchema = resolveCompositeSchema(schema, inputSchema); - const fieldConfig: SchemaFieldConfig = { - name, - schema, - resolvedSchema, - isRequired: requiredSet.has(name), - }; - if (fieldConfig.isRequired) { - requiredFields.push(fieldConfig); + if (uiMode === "chat") { + // Chat mode 3-tier: + // x-field-display override > required array > default heuristic + const displayHint = resolvedSchema["x-field-display"]; + const fieldConfig: SchemaFieldConfig = { + name, + schema, + resolvedSchema, + isRequired: displayHint === "required", + }; + + if (displayHint === "required") { + requiredFields.push(fieldConfig); + } else if (displayHint === "inline") { + optionalFields.push(fieldConfig); + normalFields.push(fieldConfig); + } else if (displayHint === "advanced") { + optionalFields.push(fieldConfig); + notRequiredFields.push(fieldConfig); + } else if (requiredSet.has(name)) { + // In required array (default TypedDict field) → Normal + optionalFields.push(fieldConfig); + normalFields.push(fieldConfig); + } else { + // Not in required array (Python NotRequired) → NotRequired + optionalFields.push(fieldConfig); + notRequiredFields.push(fieldConfig); + } } else { - optionalFields.push(fieldConfig); + // Form mode: 2-tier based on JSON Schema required array + const fieldConfig: SchemaFieldConfig = { + name, + schema, + resolvedSchema, + isRequired: requiredSet.has(name), + }; + + if (fieldConfig.isRequired) { + requiredFields.push(fieldConfig); + } else { + optionalFields.push(fieldConfig); + } } } @@ -258,6 +329,8 @@ export function parseInputSchema( uiMode, requiredFields, optionalFields, + normalFields, + notRequiredFields, hasMessages, rawSchema: inputSchema, }; diff --git a/frontend/src/types/schema-ui.ts b/frontend/src/types/schema-ui.ts index ab1270f..951eabe 100644 --- a/frontend/src/types/schema-ui.ts +++ b/frontend/src/types/schema-ui.ts @@ -21,6 +21,8 @@ export interface JSONSchemaProperty { maximum?: number; pattern?: string; format?: string; + /** Custom display hint for chat mode field classification */ + "x-field-display"?: "required" | "inline" | "advanced"; } export interface JSONSchema { @@ -56,7 +58,24 @@ export interface SchemaFieldConfig { export interface ParsedInputSchema { uiMode: UIMode; requiredFields: SchemaFieldConfig[]; + /** + * All non-required fields regardless of display tier. + * This is the union of normalFields + notRequiredFields. + * Kept for backward compatibility with buildSubmitPayload, resetForm, etc. + */ optionalFields: SchemaFieldConfig[]; + /** + * Chat mode only: optional fields that should display inline above the input box. + * These are fields without a default or with an empty default (null, "", [], {}). + * Empty array in form mode. + */ + normalFields: SchemaFieldConfig[]; + /** + * Chat mode only: optional fields that should display in "Advanced Input" only. + * These are fields with a non-empty default value. + * Empty array in form mode. + */ + notRequiredFields: SchemaFieldConfig[]; hasMessages: boolean; rawSchema: JSONSchema | null; } From 60089af9aab83beea9d33a6147aead8c7df52207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=AF=BC=EC=84=9D?= Date: Wed, 15 Apr 2026 13:17:44 +0900 Subject: [PATCH 2/2] style: fix prettier formatting in InlineFieldsSection --- .../chat/components/schema-ui/InlineFieldsSection.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/features/chat/components/schema-ui/InlineFieldsSection.tsx b/frontend/src/features/chat/components/schema-ui/InlineFieldsSection.tsx index 9283ca0..a04b1f3 100644 --- a/frontend/src/features/chat/components/schema-ui/InlineFieldsSection.tsx +++ b/frontend/src/features/chat/components/schema-ui/InlineFieldsSection.tsx @@ -7,7 +7,11 @@ import React from "react"; import { AnimatePresence, motion } from "framer-motion"; import { SchemaField } from "./SchemaField"; -import type { SchemaFieldConfig, FieldValue, JSONSchema } from "@/types/schema-ui"; +import type { + SchemaFieldConfig, + FieldValue, + JSONSchema, +} from "@/types/schema-ui"; interface InlineFieldsSectionProps { requiredFields: SchemaFieldConfig[];