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;