@streamoid/chat-components 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +9 -2
- package/dist/index.js +168 -5
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -10,9 +10,10 @@ interface TodoItem {
|
|
|
10
10
|
content: string;
|
|
11
11
|
status: "pending" | "completed";
|
|
12
12
|
}
|
|
13
|
+
type MediaType = "image" | "video" | "mixed";
|
|
13
14
|
interface DynamicFormField {
|
|
14
15
|
id: string;
|
|
15
|
-
type: "text" | "textarea" | "number" | "select" | "multiselect" | "checkbox" | "radio" | "approval" | "todo_list" | "file";
|
|
16
|
+
type: "text" | "textarea" | "number" | "select" | "multiselect" | "checkbox" | "radio" | "approval" | "media_approval" | "todo_list" | "file";
|
|
16
17
|
label?: string;
|
|
17
18
|
description?: string;
|
|
18
19
|
placeholder?: string;
|
|
@@ -22,6 +23,12 @@ interface DynamicFormField {
|
|
|
22
23
|
items?: TodoItem[];
|
|
23
24
|
accept?: string;
|
|
24
25
|
multiple?: boolean;
|
|
26
|
+
media_urls?: string[];
|
|
27
|
+
media_type?: MediaType;
|
|
28
|
+
approve_label?: string;
|
|
29
|
+
reject_label?: string;
|
|
30
|
+
feedback_label?: string;
|
|
31
|
+
feedback_placeholder?: string;
|
|
25
32
|
}
|
|
26
33
|
interface DynamicFormProps {
|
|
27
34
|
workspaceId: string;
|
|
@@ -55,7 +62,7 @@ declare const dynamicFormManifest: {
|
|
|
55
62
|
readonly name: "common:dynamic-form";
|
|
56
63
|
readonly aliases: readonly ["dynamic_form"];
|
|
57
64
|
readonly service: "common";
|
|
58
|
-
readonly description: "Server-driven dynamic form supporting text, textarea, number, select, multiselect, checkbox, radio, approval, todo_list, and file upload fields";
|
|
65
|
+
readonly description: "Server-driven dynamic form supporting text, textarea, number, select, multiselect, checkbox, radio, approval, media_approval, todo_list, and file upload fields";
|
|
59
66
|
readonly containerStyle: {
|
|
60
67
|
readonly display: "flex";
|
|
61
68
|
readonly position: "relative";
|
package/dist/index.js
CHANGED
|
@@ -417,6 +417,46 @@ function DynamicForm(props) {
|
|
|
417
417
|
const fields = uiProps.fields ?? [];
|
|
418
418
|
const submitText = uiProps.submit_button_text ?? "Submit";
|
|
419
419
|
const cancelText = uiProps.cancel_button_text;
|
|
420
|
+
const getTextValue = (value, fallback = "") => {
|
|
421
|
+
if (typeof value === "string") return value;
|
|
422
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
423
|
+
if (value && typeof value === "object") {
|
|
424
|
+
const obj = value;
|
|
425
|
+
if (typeof obj.label === "string") return obj.label;
|
|
426
|
+
if (typeof obj.text === "string") return obj.text;
|
|
427
|
+
if (typeof obj.value === "string") return obj.value;
|
|
428
|
+
}
|
|
429
|
+
return fallback;
|
|
430
|
+
};
|
|
431
|
+
const normalizeMediaUrls = (raw) => {
|
|
432
|
+
if (Array.isArray(raw)) {
|
|
433
|
+
return raw.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
434
|
+
}
|
|
435
|
+
if (typeof raw === "string" && raw.trim().length > 0) {
|
|
436
|
+
const maybeJson = raw.trim();
|
|
437
|
+
if (maybeJson.startsWith("[") && maybeJson.endsWith("]")) {
|
|
438
|
+
try {
|
|
439
|
+
const parsed = JSON.parse(maybeJson);
|
|
440
|
+
if (Array.isArray(parsed)) {
|
|
441
|
+
return parsed.filter(
|
|
442
|
+
(item) => typeof item === "string" && item.trim().length > 0
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
} catch {
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return maybeJson.split(/[\n,]/).map((part) => part.trim()).filter(Boolean);
|
|
449
|
+
}
|
|
450
|
+
return [];
|
|
451
|
+
};
|
|
452
|
+
const inferMediaTypeFromUrl = (url) => {
|
|
453
|
+
const cleanUrl = (url || "").split("?")[0].toLowerCase();
|
|
454
|
+
const videoExts = [".mp4", ".mov", ".webm", ".m4v", ".avi", ".mkv"];
|
|
455
|
+
const imageExts = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg", ".heic"];
|
|
456
|
+
if (videoExts.some((ext) => cleanUrl.endsWith(ext))) return "video";
|
|
457
|
+
if (imageExts.some((ext) => cleanUrl.endsWith(ext))) return "image";
|
|
458
|
+
return "image";
|
|
459
|
+
};
|
|
420
460
|
const getInitialValues = () => {
|
|
421
461
|
if (submittedValues) return { ...submittedValues };
|
|
422
462
|
const values = {};
|
|
@@ -429,6 +469,14 @@ function DynamicForm(props) {
|
|
|
429
469
|
values[field.id] = field.items.map((item) => ({ ...item }));
|
|
430
470
|
} else if (field.type === "approval") {
|
|
431
471
|
values[field.id] = null;
|
|
472
|
+
} else if (field.type === "media_approval") {
|
|
473
|
+
const mediaUrls = normalizeMediaUrls(field.media_urls);
|
|
474
|
+
values[field.id] = {
|
|
475
|
+
decision: null,
|
|
476
|
+
feedback: "",
|
|
477
|
+
media_urls: mediaUrls,
|
|
478
|
+
media_type: field.media_type ?? "mixed"
|
|
479
|
+
};
|
|
432
480
|
} else {
|
|
433
481
|
values[field.id] = "";
|
|
434
482
|
}
|
|
@@ -629,6 +677,31 @@ function DynamicForm(props) {
|
|
|
629
677
|
handleFieldChange(fieldId, action);
|
|
630
678
|
await submitForm(approved ? "Approve" : "Reject", { ...formValues, [fieldId]: action });
|
|
631
679
|
};
|
|
680
|
+
const handleMediaApprovalDecision = async (field, decision) => {
|
|
681
|
+
const current = formValues[field.id] ?? {
|
|
682
|
+
decision: null,
|
|
683
|
+
feedback: "",
|
|
684
|
+
media_urls: Array.isArray(field.media_urls) ? field.media_urls : [],
|
|
685
|
+
media_type: field.media_type ?? "mixed"
|
|
686
|
+
};
|
|
687
|
+
const feedback = current.feedback?.trim() ?? "";
|
|
688
|
+
if (decision === "needs_modification" && !feedback) {
|
|
689
|
+
setError("Feedback is required when requesting modifications.");
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
const nextValue = {
|
|
693
|
+
...current,
|
|
694
|
+
decision,
|
|
695
|
+
feedback,
|
|
696
|
+
media_urls: current.media_urls ?? (Array.isArray(field.media_urls) ? field.media_urls : []),
|
|
697
|
+
media_type: current.media_type ?? field.media_type ?? "mixed"
|
|
698
|
+
};
|
|
699
|
+
handleFieldChange(field.id, nextValue);
|
|
700
|
+
await submitForm(
|
|
701
|
+
decision === "approved" ? "Approve" : "Needs Modification",
|
|
702
|
+
{ ...formValues, [field.id]: nextValue }
|
|
703
|
+
);
|
|
704
|
+
};
|
|
632
705
|
const validateForm = () => {
|
|
633
706
|
for (const field of fields) {
|
|
634
707
|
if (field.required) {
|
|
@@ -653,7 +726,13 @@ function DynamicForm(props) {
|
|
|
653
726
|
const field = fields.find((f) => f.id === fieldId);
|
|
654
727
|
const fieldLabel = field?.label || fieldId;
|
|
655
728
|
let displayVal;
|
|
656
|
-
if (field?.
|
|
729
|
+
if (field?.type === "media_approval" && val && typeof val === "object") {
|
|
730
|
+
const mediaVal = val;
|
|
731
|
+
const decisionText = mediaVal.decision ?? "pending";
|
|
732
|
+
const feedbackText = mediaVal.feedback?.trim() || "none";
|
|
733
|
+
const mediaCount = Array.isArray(mediaVal.media_urls) ? mediaVal.media_urls.length : 0;
|
|
734
|
+
displayVal = `decision=${decisionText}, feedback=${feedbackText}, media_count=${mediaCount}`;
|
|
735
|
+
} else if (field?.options && typeof val === "string") {
|
|
657
736
|
const opt = field.options.find((o) => o.id === val);
|
|
658
737
|
displayVal = opt?.label || val;
|
|
659
738
|
} else if (field?.options && Array.isArray(val)) {
|
|
@@ -811,6 +890,88 @@ ${valueSummaryParts.join("\n")}` : "";
|
|
|
811
890
|
}
|
|
812
891
|
)
|
|
813
892
|
] });
|
|
893
|
+
case "media_approval": {
|
|
894
|
+
const mediaValue = formValues[field.id] ?? {
|
|
895
|
+
decision: null,
|
|
896
|
+
feedback: "",
|
|
897
|
+
media_urls: normalizeMediaUrls(field.media_urls),
|
|
898
|
+
media_type: field.media_type ?? "mixed"
|
|
899
|
+
};
|
|
900
|
+
const mediaUrls = normalizeMediaUrls(field.media_urls ?? mediaValue.media_urls);
|
|
901
|
+
const feedbackLabel = field.feedback_label ?? "What should be changed?";
|
|
902
|
+
const feedbackPlaceholder = field.feedback_placeholder ?? "Describe what needs to be modified.";
|
|
903
|
+
const approveLabel = field.approve_label ?? "Approve";
|
|
904
|
+
const rejectLabel = field.reject_label ?? "Needs Modification";
|
|
905
|
+
return /* @__PURE__ */ jsxs2("div", { className: "space-y-3", children: [
|
|
906
|
+
mediaUrls.length > 0 && /* @__PURE__ */ jsx10("div", { className: "grid gap-2 sm:grid-cols-2", children: mediaUrls.map((url, idx) => {
|
|
907
|
+
const mediaType = field.media_type && field.media_type !== "mixed" ? field.media_type : inferMediaTypeFromUrl(url);
|
|
908
|
+
return /* @__PURE__ */ jsx10(
|
|
909
|
+
"div",
|
|
910
|
+
{
|
|
911
|
+
className: "rounded-md border border-border overflow-hidden bg-muted/20",
|
|
912
|
+
children: mediaType === "video" ? /* @__PURE__ */ jsx10("video", { src: url, controls: true, className: "w-full h-44 object-cover bg-black" }) : /* @__PURE__ */ jsx10(
|
|
913
|
+
"img",
|
|
914
|
+
{
|
|
915
|
+
src: url,
|
|
916
|
+
alt: `Generated media ${idx + 1}`,
|
|
917
|
+
className: "w-full h-44 object-cover",
|
|
918
|
+
loading: "lazy"
|
|
919
|
+
}
|
|
920
|
+
)
|
|
921
|
+
},
|
|
922
|
+
`${field.id}-${idx}`
|
|
923
|
+
);
|
|
924
|
+
}) }),
|
|
925
|
+
/* @__PURE__ */ jsxs2("div", { className: "space-y-1", children: [
|
|
926
|
+
/* @__PURE__ */ jsx10(Label2, { className: "text-xs font-medium", children: feedbackLabel }),
|
|
927
|
+
/* @__PURE__ */ jsx10(
|
|
928
|
+
Textarea,
|
|
929
|
+
{
|
|
930
|
+
value: mediaValue.feedback || "",
|
|
931
|
+
onChange: (e) => handleFieldChange(field.id, {
|
|
932
|
+
...mediaValue,
|
|
933
|
+
feedback: e.target.value,
|
|
934
|
+
media_urls: normalizeMediaUrls(mediaValue.media_urls ?? mediaUrls),
|
|
935
|
+
media_type: mediaValue.media_type ?? field.media_type ?? "mixed"
|
|
936
|
+
}),
|
|
937
|
+
placeholder: feedbackPlaceholder,
|
|
938
|
+
className: "bg-background min-h-[72px] w-full text-xs"
|
|
939
|
+
}
|
|
940
|
+
)
|
|
941
|
+
] }),
|
|
942
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex gap-2 justify-end", children: [
|
|
943
|
+
/* @__PURE__ */ jsxs2(
|
|
944
|
+
Button,
|
|
945
|
+
{
|
|
946
|
+
type: "button",
|
|
947
|
+
variant: "destructive",
|
|
948
|
+
size: "sm",
|
|
949
|
+
onClick: () => handleMediaApprovalDecision(field, "needs_modification"),
|
|
950
|
+
className: "bg-destructive/10 text-destructive hover:bg-destructive/20 border-transparent shadow-none h-8 text-xs",
|
|
951
|
+
disabled: isLoading,
|
|
952
|
+
children: [
|
|
953
|
+
/* @__PURE__ */ jsx10(AlertCircle, { className: "w-3 h-3 mr-1.5" }),
|
|
954
|
+
rejectLabel
|
|
955
|
+
]
|
|
956
|
+
}
|
|
957
|
+
),
|
|
958
|
+
/* @__PURE__ */ jsxs2(
|
|
959
|
+
Button,
|
|
960
|
+
{
|
|
961
|
+
type: "button",
|
|
962
|
+
size: "sm",
|
|
963
|
+
onClick: () => handleMediaApprovalDecision(field, "approved"),
|
|
964
|
+
className: "bg-emerald-600 hover:bg-emerald-700 text-white shadow-none border-transparent h-8 text-xs",
|
|
965
|
+
disabled: isLoading,
|
|
966
|
+
children: [
|
|
967
|
+
/* @__PURE__ */ jsx10(CheckCircle, { className: "w-3 h-3 mr-1.5" }),
|
|
968
|
+
approveLabel
|
|
969
|
+
]
|
|
970
|
+
}
|
|
971
|
+
)
|
|
972
|
+
] })
|
|
973
|
+
] });
|
|
974
|
+
}
|
|
814
975
|
case "todo_list": {
|
|
815
976
|
const todoItems = formValues[field.id] || [];
|
|
816
977
|
const completedCount = todoItems.filter((i) => i.status === "completed").length;
|
|
@@ -1058,12 +1219,14 @@ ${valueSummaryParts.join("\n")}` : "";
|
|
|
1058
1219
|
children: /* @__PURE__ */ jsxs2("div", { className: "space-y-3 pb-2", children: [
|
|
1059
1220
|
fields.map((field) => {
|
|
1060
1221
|
const statusText = getFieldStatusText(field);
|
|
1222
|
+
const labelText = getTextValue(field.label, field.id);
|
|
1223
|
+
const descriptionText = getTextValue(field.description, "");
|
|
1061
1224
|
return /* @__PURE__ */ jsxs2("div", { className: "space-y-1", children: [
|
|
1062
1225
|
/* @__PURE__ */ jsxs2(Label2, { className: "text-xs font-medium flex items-center gap-1.5", children: [
|
|
1063
|
-
/* @__PURE__ */ jsx10("span", { children:
|
|
1226
|
+
/* @__PURE__ */ jsx10("span", { children: labelText }),
|
|
1064
1227
|
field.required && /* @__PURE__ */ jsx10("span", { className: "text-destructive text-[10px]", children: "*" })
|
|
1065
1228
|
] }),
|
|
1066
|
-
|
|
1229
|
+
descriptionText && /* @__PURE__ */ jsx10("p", { className: "text-[11px] text-muted-foreground leading-snug", children: descriptionText }),
|
|
1067
1230
|
renderField(field),
|
|
1068
1231
|
statusText && field.type !== "file" && field.type !== "todo_list" && /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1 text-emerald-600 mt-0.5", children: [
|
|
1069
1232
|
/* @__PURE__ */ jsx10(CheckCircle, { className: "w-2.5 h-2.5" }),
|
|
@@ -1079,7 +1242,7 @@ ${valueSummaryParts.join("\n")}` : "";
|
|
|
1079
1242
|
}
|
|
1080
1243
|
),
|
|
1081
1244
|
/* @__PURE__ */ jsx10("div", { className: `scroll-fade-bottom ${!canScrollDown ? "fade-hidden" : ""}` }),
|
|
1082
|
-
/* @__PURE__ */ jsxs2("div", { className: "flex justify-end gap-2 px-5 py-2 flex-shrink-0", children: [
|
|
1245
|
+
!(fields.length === 1 && fields[0]?.type === "media_approval") && /* @__PURE__ */ jsxs2("div", { className: "flex justify-end gap-2 px-5 py-2 flex-shrink-0", children: [
|
|
1083
1246
|
cancelText && /* @__PURE__ */ jsx10(
|
|
1084
1247
|
Button,
|
|
1085
1248
|
{
|
|
@@ -1113,7 +1276,7 @@ var dynamicFormManifest = {
|
|
|
1113
1276
|
name: "common:dynamic-form",
|
|
1114
1277
|
aliases: ["dynamic_form"],
|
|
1115
1278
|
service: "common",
|
|
1116
|
-
description: "Server-driven dynamic form supporting text, textarea, number, select, multiselect, checkbox, radio, approval, todo_list, and file upload fields",
|
|
1279
|
+
description: "Server-driven dynamic form supporting text, textarea, number, select, multiselect, checkbox, radio, approval, media_approval, todo_list, and file upload fields",
|
|
1117
1280
|
containerStyle: {
|
|
1118
1281
|
display: "flex",
|
|
1119
1282
|
position: "relative",
|
package/package.json
CHANGED