Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions docs-site/content/docs/reference/configuration.ko.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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` 파일은 앱의 외형과 동작을 제어합니다.
Expand Down
67 changes: 67 additions & 0 deletions docs-site/content/docs/reference/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* 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<string, FieldValue>;
displayState: Record<string, FieldValue>;
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 (
<AnimatePresence>
<motion.div
initial={{ opacity: 0, y: -5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.15 }}
className="min-w-0 space-y-2 overflow-hidden px-4 pt-3"
>
{/* Required fields */}
{hasRequired && (
<div className="space-y-2">
{requiredFields.map((field) => (
<SchemaField
key={field.name}
field={field}
rootSchema={rootSchema}
value={formState[field.name]}
displayValue={displayState[field.name]}
onChange={(value) => onFieldChange(field.name, value)}
onDisplayValueChange={(value) =>
onDisplayValueChange(field.name, value)
}
disabled={disabled}
compact
fileUploadMode={fileUploadMode}
/>
))}
</div>
)}

{/* Separator between required and normal sections */}
{hasRequired && hasNormal && (
<div className="border-border/30 border-t" />
)}

{/* Normal (optional) fields */}
{hasNormal && (
<div className="space-y-2">
{normalFields.map((field) => (
<SchemaField
key={field.name}
field={field}
rootSchema={rootSchema}
value={formState[field.name]}
displayValue={displayState[field.name]}
onChange={(value) => onFieldChange(field.name, value)}
onDisplayValueChange={(value) =>
onDisplayValueChange(field.name, value)
}
disabled={disabled}
compact
fileUploadMode={fileUploadMode}
/>
))}
</div>
)}
</motion.div>
</AnimatePresence>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,20 @@ 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";
}

export function SchemaFieldsSection({
schemaUI,
fields: fieldsProp,
disabled = false,
className,
fileUploadMode,
Expand All @@ -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;
}

Expand Down Expand Up @@ -67,7 +74,7 @@ export function SchemaFieldsSection({
<ChevronDown className="h-3.5 w-3.5" />
</motion.span>
<span className="text-muted-foreground/60 text-xs">
({optionalFields.length})
({displayFields.length})
</span>
</button>

Expand All @@ -81,7 +88,7 @@ export function SchemaFieldsSection({
className="overflow-hidden"
>
<div className="border-muted max-h-[200px] space-y-2 overflow-y-auto border-l-2 pt-1 pl-3">
{optionalFields.map((field) => (
{displayFields.map((field) => (
<SchemaField
key={field.name}
field={field}
Expand Down
Loading
Loading