@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 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?.options && typeof val === "string") {
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: field.label }),
1226
+ /* @__PURE__ */ jsx10("span", { children: labelText }),
1064
1227
  field.required && /* @__PURE__ */ jsx10("span", { className: "text-destructive text-[10px]", children: "*" })
1065
1228
  ] }),
1066
- field.description && /* @__PURE__ */ jsx10("p", { className: "text-[11px] text-muted-foreground leading-snug", children: field.description }),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamoid/chat-components",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "Shared chat UI components for the Streamoid chat host — DynamicForm and other cross-service components",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",