From c19ab0f80e9251461f8f5407180b7b299ba637f3 Mon Sep 17 00:00:00 2001 From: Prashant Vasudevan <71649489+vprashrex@users.noreply.github.com> Date: Sun, 25 Oct 2026 12:15:09 +0530 Subject: [PATCH 1/2] feat(assessment): Support video attachment columns Add "video" as an assessment field type alongside image and pdf. It is offered in the @ mention type picker as "Video (link)", gets its own icon in the dropdown, preview and review modal, and round-trips through the config blob input_schema as {type: "video", format: "url"}. Shared icon selection moves into FieldTypeIcon so the three call sites stay in sync. --- .../assessment/editor/FieldTypeIcon.tsx | 14 +++++++++++ .../assessment/editor/MentionDropdown.tsx | 16 +++++++------ .../assessment/editor/PreviewPane.tsx | 17 +++++-------- .../assessment/wizard/ReviewSaveModal.tsx | 6 ++--- app/components/icons/common/VideoIcon.tsx | 24 +++++++++++++++++++ app/components/icons/index.tsx | 1 + app/hooks/usePromptMentions.ts | 2 +- app/lib/assessment/configBlob.ts | 2 +- app/lib/assessment/promptTokens.ts | 1 + app/lib/types/assessment/core.ts | 2 +- app/lib/types/assessment/prompt.ts | 2 +- 11 files changed, 61 insertions(+), 26 deletions(-) create mode 100644 app/components/assessment/editor/FieldTypeIcon.tsx create mode 100644 app/components/icons/common/VideoIcon.tsx diff --git a/app/components/assessment/editor/FieldTypeIcon.tsx b/app/components/assessment/editor/FieldTypeIcon.tsx new file mode 100644 index 00000000..6681e338 --- /dev/null +++ b/app/components/assessment/editor/FieldTypeIcon.tsx @@ -0,0 +1,14 @@ +import { DocumentFileIcon, ImageIcon, VideoIcon } from "@/app/components/icons"; +import type { PromptFieldType } from "@/app/lib/types/assessment"; + +export default function FieldTypeIcon({ + type, + className, +}: { + type: PromptFieldType; + className: string; +}) { + if (type === "image") return ; + if (type === "video") return ; + return ; +} diff --git a/app/components/assessment/editor/MentionDropdown.tsx b/app/components/assessment/editor/MentionDropdown.tsx index 666186bb..d5abb4ba 100644 --- a/app/components/assessment/editor/MentionDropdown.tsx +++ b/app/components/assessment/editor/MentionDropdown.tsx @@ -2,17 +2,19 @@ import { useLayoutEffect, useState } from "react"; import { createPortal } from "react-dom"; -import { DocumentFileIcon, ImageIcon } from "@/app/components/icons"; +import FieldTypeIcon from "./FieldTypeIcon"; import type { MentionDropdownProps, PromptFieldType, } from "@/app/lib/types/assessment"; -const TYPE_ICONS: Record = { - text: Aa, - image: , - pdf: , -}; +function typeBadge(type: PromptFieldType): React.ReactNode { + return type === "text" ? ( + Aa + ) : ( + + ); +} const rowBase = "flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-left text-sm"; @@ -78,7 +80,7 @@ export default function MentionDropdown({ onPickType(option.type)} diff --git a/app/components/assessment/editor/PreviewPane.tsx b/app/components/assessment/editor/PreviewPane.tsx index c9147073..b7db4618 100644 --- a/app/components/assessment/editor/PreviewPane.tsx +++ b/app/components/assessment/editor/PreviewPane.tsx @@ -1,10 +1,7 @@ "use client"; -import { - DocumentFileIcon, - ImageIcon, - LockClosedIcon, -} from "@/app/components/icons"; +import { LockClosedIcon } from "@/app/components/icons"; +import FieldTypeIcon from "./FieldTypeIcon"; import { buildPreviewBlocks, fieldTypeOf, @@ -115,14 +112,12 @@ function SegmentView({ if (segment.kind === "plain") return <>{segment.text}; if (segment.kind === "attachment" && segment.name) { - const isImage = fieldTypeOf(fieldTypes, segment.name) === "image"; return ( - {isImage ? ( - - ) : ( - - )} + {segment.name} ); diff --git a/app/components/assessment/wizard/ReviewSaveModal.tsx b/app/components/assessment/wizard/ReviewSaveModal.tsx index 62550fb7..05d6721f 100644 --- a/app/components/assessment/wizard/ReviewSaveModal.tsx +++ b/app/components/assessment/wizard/ReviewSaveModal.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { Button, Field, Modal } from "@/app/components/ui"; -import { DocumentFileIcon, ImageIcon } from "@/app/components/icons"; +import FieldTypeIcon from "@/app/components/assessment/editor/FieldTypeIcon"; import { fieldTypeOf, TOKEN_RE, @@ -63,10 +63,8 @@ export default function ReviewSaveModal({ > {type === "text" ? ( "@" - ) : type === "image" ? ( - ) : ( - + )} {column} diff --git a/app/components/icons/common/VideoIcon.tsx b/app/components/icons/common/VideoIcon.tsx new file mode 100644 index 00000000..d5ef033c --- /dev/null +++ b/app/components/icons/common/VideoIcon.tsx @@ -0,0 +1,24 @@ +interface IconProps { + className?: string; + style?: React.CSSProperties; +} + +export default function VideoIcon({ className, style }: IconProps) { + return ( + + + + + ); +} diff --git a/app/components/icons/index.tsx b/app/components/icons/index.tsx index d4b6d4d7..ad68c968 100644 --- a/app/components/icons/index.tsx +++ b/app/components/icons/index.tsx @@ -23,6 +23,7 @@ export { default as ErrorCircleIcon } from "./common/ErrorCircleIcon"; export { default as DownloadIcon } from "./common/DownloadIcon"; export { default as HomeIcon } from "./common/HomeIcon"; export { default as ImageIcon } from "./common/ImageIcon"; +export { default as VideoIcon } from "./common/VideoIcon"; export { default as LockClosedIcon } from "./common/LockClosedIcon"; // Evaluations Icons diff --git a/app/hooks/usePromptMentions.ts b/app/hooks/usePromptMentions.ts index 515491a3..6cf8497f 100644 --- a/app/hooks/usePromptMentions.ts +++ b/app/hooks/usePromptMentions.ts @@ -17,7 +17,7 @@ import type { UsePromptMentionsResult, } from "@/app/lib/types/assessment"; -const TYPE_ORDER: PromptFieldType[] = ["text", "image", "pdf"]; +const TYPE_ORDER: PromptFieldType[] = ["text", "image", "pdf", "video"]; const HOVER_CLOSE_DELAY_MS = 200; diff --git a/app/lib/assessment/configBlob.ts b/app/lib/assessment/configBlob.ts index 519c10ef..4031bc3c 100644 --- a/app/lib/assessment/configBlob.ts +++ b/app/lib/assessment/configBlob.ts @@ -62,7 +62,7 @@ function attachmentsFrom( .filter(([, column]) => column.type !== "text") .map(([name, column]) => ({ column: name, - type: column.type as "image" | "pdf", + type: column.type as Exclude, format: column.format ?? "url", })); } diff --git a/app/lib/assessment/promptTokens.ts b/app/lib/assessment/promptTokens.ts index deea3c53..712b11fa 100644 --- a/app/lib/assessment/promptTokens.ts +++ b/app/lib/assessment/promptTokens.ts @@ -13,6 +13,7 @@ export const PROMPT_FIELD_TYPE_LABELS: Record = { text: "Text", image: "Image (link)", pdf: "PDF (link)", + video: "Video (link)", }; export function tokenName(raw: string): string { diff --git a/app/lib/types/assessment/core.ts b/app/lib/types/assessment/core.ts index b9606adb..4dc6f6c0 100644 --- a/app/lib/types/assessment/core.ts +++ b/app/lib/types/assessment/core.ts @@ -126,7 +126,7 @@ export interface AssessmentResultsPayload { export interface Attachment { column: string; - type: "image" | "pdf" | "mixed"; + type: "image" | "pdf" | "video" | "mixed"; format: "url" | "base64"; type_column?: string | null; type_value_map?: Record | null; diff --git a/app/lib/types/assessment/prompt.ts b/app/lib/types/assessment/prompt.ts index 67d4c599..a171b281 100644 --- a/app/lib/types/assessment/prompt.ts +++ b/app/lib/types/assessment/prompt.ts @@ -3,7 +3,7 @@ import type { ProviderType } from "@/app/lib/types/configs"; import type { SchemaProperty } from "./core"; import type { AssessorModelSelection, AssessorVersionDetail } from "./assessor"; -export type PromptFieldType = "text" | "image" | "pdf"; +export type PromptFieldType = "text" | "image" | "pdf" | "video"; export type PromptZoneId = "instructions" | "submission"; From 1f9693facdc9a88920bc35e613af69f1835313db Mon Sep 17 00:00:00 2001 From: Prashant Vasudevan <71649489+vprashrex@users.noreply.github.com> Date: Thu, 29 Oct 2026 11:59:57 +0530 Subject: [PATCH 2/2] refactor(assessment): Address review on video field types - Type Attachment.type as PromptFieldType. The "mixed" variant was never produced, so its guards now check for "text" instead. - Rename FieldTypeIcon to RenderFieldType and move the text-type badge into it, so every field-type branch lives in one component. Callers pass textLabel where the text badge differs ("@" in Review & save). --- .../assessment/editor/MentionDropdown.tsx | 19 +++++-------------- .../assessment/editor/PreviewPane.tsx | 4 ++-- ...{FieldTypeIcon.tsx => RenderFieldType.tsx} | 9 ++++++++- .../assessment/wizard/ReviewSaveModal.tsx | 12 ++++++------ app/lib/assessment/configBlob.ts | 4 ++-- app/lib/assessment/promptTokens.ts | 6 +++--- app/lib/types/assessment/core.ts | 3 ++- 7 files changed, 28 insertions(+), 29 deletions(-) rename app/components/assessment/editor/{FieldTypeIcon.tsx => RenderFieldType.tsx} (66%) diff --git a/app/components/assessment/editor/MentionDropdown.tsx b/app/components/assessment/editor/MentionDropdown.tsx index d5abb4ba..b6e27bd7 100644 --- a/app/components/assessment/editor/MentionDropdown.tsx +++ b/app/components/assessment/editor/MentionDropdown.tsx @@ -2,19 +2,8 @@ import { useLayoutEffect, useState } from "react"; import { createPortal } from "react-dom"; -import FieldTypeIcon from "./FieldTypeIcon"; -import type { - MentionDropdownProps, - PromptFieldType, -} from "@/app/lib/types/assessment"; - -function typeBadge(type: PromptFieldType): React.ReactNode { - return type === "text" ? ( - Aa - ) : ( - - ); -} +import RenderFieldType from "./RenderFieldType"; +import type { MentionDropdownProps } from "@/app/lib/types/assessment"; const rowBase = "flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-left text-sm"; @@ -80,7 +69,9 @@ export default function MentionDropdown({ + } label={option.label} isCurrent={option.isCurrent} onPick={() => onPickType(option.type)} diff --git a/app/components/assessment/editor/PreviewPane.tsx b/app/components/assessment/editor/PreviewPane.tsx index b7db4618..021d8d59 100644 --- a/app/components/assessment/editor/PreviewPane.tsx +++ b/app/components/assessment/editor/PreviewPane.tsx @@ -1,7 +1,7 @@ "use client"; import { LockClosedIcon } from "@/app/components/icons"; -import FieldTypeIcon from "./FieldTypeIcon"; +import RenderFieldType from "./RenderFieldType"; import { buildPreviewBlocks, fieldTypeOf, @@ -114,7 +114,7 @@ function SegmentView({ if (segment.kind === "attachment" && segment.name) { return ( - diff --git a/app/components/assessment/editor/FieldTypeIcon.tsx b/app/components/assessment/editor/RenderFieldType.tsx similarity index 66% rename from app/components/assessment/editor/FieldTypeIcon.tsx rename to app/components/assessment/editor/RenderFieldType.tsx index 6681e338..7b0b42bc 100644 --- a/app/components/assessment/editor/FieldTypeIcon.tsx +++ b/app/components/assessment/editor/RenderFieldType.tsx @@ -1,13 +1,20 @@ import { DocumentFileIcon, ImageIcon, VideoIcon } from "@/app/components/icons"; import type { PromptFieldType } from "@/app/lib/types/assessment"; -export default function FieldTypeIcon({ +export default function RenderFieldType({ type, className, + textLabel = "Aa", }: { type: PromptFieldType; className: string; + textLabel?: string; }) { + if (type === "text") { + return ( + {textLabel} + ); + } if (type === "image") return ; if (type === "video") return ; return ; diff --git a/app/components/assessment/wizard/ReviewSaveModal.tsx b/app/components/assessment/wizard/ReviewSaveModal.tsx index 05d6721f..263846c7 100644 --- a/app/components/assessment/wizard/ReviewSaveModal.tsx +++ b/app/components/assessment/wizard/ReviewSaveModal.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { Button, Field, Modal } from "@/app/components/ui"; -import FieldTypeIcon from "@/app/components/assessment/editor/FieldTypeIcon"; +import RenderFieldType from "@/app/components/assessment/editor/RenderFieldType"; import { fieldTypeOf, TOKEN_RE, @@ -61,11 +61,11 @@ export default function ReviewSaveModal({ key={column} className="inline-flex items-center gap-1.5 rounded-full border border-border bg-bg-secondary px-2.5 py-1 font-mono text-[11px] text-text-primary" > - {type === "text" ? ( - "@" - ) : ( - - )} + {column} {type} diff --git a/app/lib/assessment/configBlob.ts b/app/lib/assessment/configBlob.ts index 4031bc3c..bc3d0f61 100644 --- a/app/lib/assessment/configBlob.ts +++ b/app/lib/assessment/configBlob.ts @@ -62,7 +62,7 @@ function attachmentsFrom( .filter(([, column]) => column.type !== "text") .map(([name, column]) => ({ column: name, - type: column.type as Exclude, + type: column.type, format: column.format ?? "url", })); } @@ -156,7 +156,7 @@ function inputSchema( names.map((name) => { const attachment = byColumn.get(name); const column: AssessmentBlobInputColumn = - attachment && attachment.type !== "mixed" + attachment && attachment.type !== "text" ? { type: attachment.type, format: attachment.format ?? "url" } : { type: "text", format: null }; return [name, strict.has(name) ? { ...column, strict: true } : column]; diff --git a/app/lib/assessment/promptTokens.ts b/app/lib/assessment/promptTokens.ts index 712b11fa..c78fbad8 100644 --- a/app/lib/assessment/promptTokens.ts +++ b/app/lib/assessment/promptTokens.ts @@ -166,7 +166,7 @@ export function fromWireTemplate( return template.replace(TOKEN_RE, (raw) => { const name = tokenName(raw); const type = byColumn.get(name); - return type && type !== "mixed" ? `<${name}>` : `@${name}`; + return type && type !== "text" ? `<${name}>` : `@${name}`; }); } @@ -175,7 +175,7 @@ export function fieldTypesFromAttachments( ): Record { return Object.fromEntries( attachments - .filter((item) => item.type !== "mixed") - .map((item) => [item.column, item.type as PromptFieldType]), + .filter((item) => item.type !== "text") + .map((item) => [item.column, item.type]), ); } diff --git a/app/lib/types/assessment/core.ts b/app/lib/types/assessment/core.ts index 4dc6f6c0..8c851c5c 100644 --- a/app/lib/types/assessment/core.ts +++ b/app/lib/types/assessment/core.ts @@ -1,4 +1,5 @@ import type { ChangeEvent, DragEvent, RefObject } from "react"; +import type { PromptFieldType } from "./prompt"; export type ValueSetter = (value: T) => void; export type SampleRow = Record; @@ -126,7 +127,7 @@ export interface AssessmentResultsPayload { export interface Attachment { column: string; - type: "image" | "pdf" | "video" | "mixed"; + type: PromptFieldType; format: "url" | "base64"; type_column?: string | null; type_value_map?: Record | null;