@streamoid/chat-components 0.2.5 → 0.2.7

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,14 @@ 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 inferMediaTypeFromUrl = (url) => {
421
+ const cleanUrl = (url || "").split("?")[0].toLowerCase();
422
+ const videoExts = [".mp4", ".mov", ".webm", ".m4v", ".avi", ".mkv"];
423
+ const imageExts = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg", ".heic"];
424
+ if (videoExts.some((ext) => cleanUrl.endsWith(ext))) return "video";
425
+ if (imageExts.some((ext) => cleanUrl.endsWith(ext))) return "image";
426
+ return "image";
427
+ };
420
428
  const getInitialValues = () => {
421
429
  if (submittedValues) return { ...submittedValues };
422
430
  const values = {};
@@ -429,6 +437,14 @@ function DynamicForm(props) {
429
437
  values[field.id] = field.items.map((item) => ({ ...item }));
430
438
  } else if (field.type === "approval") {
431
439
  values[field.id] = null;
440
+ } else if (field.type === "media_approval") {
441
+ const mediaUrls = Array.isArray(field.media_urls) ? field.media_urls.filter(Boolean) : [];
442
+ values[field.id] = {
443
+ decision: null,
444
+ feedback: "",
445
+ media_urls: mediaUrls,
446
+ media_type: field.media_type ?? "mixed"
447
+ };
432
448
  } else {
433
449
  values[field.id] = "";
434
450
  }
@@ -629,6 +645,31 @@ function DynamicForm(props) {
629
645
  handleFieldChange(fieldId, action);
630
646
  await submitForm(approved ? "Approve" : "Reject", { ...formValues, [fieldId]: action });
631
647
  };
648
+ const handleMediaApprovalDecision = async (field, decision) => {
649
+ const current = formValues[field.id] ?? {
650
+ decision: null,
651
+ feedback: "",
652
+ media_urls: Array.isArray(field.media_urls) ? field.media_urls : [],
653
+ media_type: field.media_type ?? "mixed"
654
+ };
655
+ const feedback = current.feedback?.trim() ?? "";
656
+ if (decision === "needs_modification" && !feedback) {
657
+ setError("Feedback is required when requesting modifications.");
658
+ return;
659
+ }
660
+ const nextValue = {
661
+ ...current,
662
+ decision,
663
+ feedback,
664
+ media_urls: current.media_urls ?? (Array.isArray(field.media_urls) ? field.media_urls : []),
665
+ media_type: current.media_type ?? field.media_type ?? "mixed"
666
+ };
667
+ handleFieldChange(field.id, nextValue);
668
+ await submitForm(
669
+ decision === "approved" ? "Approve" : "Needs Modification",
670
+ { ...formValues, [field.id]: nextValue }
671
+ );
672
+ };
632
673
  const validateForm = () => {
633
674
  for (const field of fields) {
634
675
  if (field.required) {
@@ -648,7 +689,25 @@ function DynamicForm(props) {
648
689
  form_title: title,
649
690
  values
650
691
  };
651
- const messageText = `User clicked "${userAction}" on form "${title}"`;
692
+ const valueSummaryParts = [];
693
+ for (const [fieldId, val] of Object.entries(values)) {
694
+ const field = fields.find((f) => f.id === fieldId);
695
+ const fieldLabel = field?.label || fieldId;
696
+ let displayVal;
697
+ if (field?.options && typeof val === "string") {
698
+ const opt = field.options.find((o) => o.id === val);
699
+ displayVal = opt?.label || val;
700
+ } else if (field?.options && Array.isArray(val)) {
701
+ displayVal = val.map((v) => field.options.find((o) => o.id === v)?.label || v).join(", ");
702
+ } else {
703
+ displayVal = String(val ?? "");
704
+ }
705
+ valueSummaryParts.push(`${fieldLabel}: ${displayVal}`);
706
+ }
707
+ const valueSummary = valueSummaryParts.length > 0 ? `
708
+ Submitted values:
709
+ ${valueSummaryParts.join("\n")}` : "";
710
+ const messageText = `User clicked "${userAction}" on form "${title}"${valueSummary}`;
652
711
  try {
653
712
  await onSubmit(resumePayload, messageText);
654
713
  setSubmittedData({ user_action: userAction, values });
@@ -793,6 +852,88 @@ function DynamicForm(props) {
793
852
  }
794
853
  )
795
854
  ] });
855
+ case "media_approval": {
856
+ const mediaUrls = Array.isArray(field.media_urls) ? field.media_urls.filter(Boolean) : [];
857
+ const mediaValue = formValues[field.id] ?? {
858
+ decision: null,
859
+ feedback: "",
860
+ media_urls: mediaUrls,
861
+ media_type: field.media_type ?? "mixed"
862
+ };
863
+ const feedbackLabel = field.feedback_label ?? "What should be changed?";
864
+ const feedbackPlaceholder = field.feedback_placeholder ?? "Describe what needs to be modified.";
865
+ const approveLabel = field.approve_label ?? "Approve";
866
+ const rejectLabel = field.reject_label ?? "Needs Modification";
867
+ return /* @__PURE__ */ jsxs2("div", { className: "space-y-3", children: [
868
+ mediaUrls.length > 0 && /* @__PURE__ */ jsx10("div", { className: "grid gap-2 sm:grid-cols-2", children: mediaUrls.map((url, idx) => {
869
+ const mediaType = field.media_type && field.media_type !== "mixed" ? field.media_type : inferMediaTypeFromUrl(url);
870
+ return /* @__PURE__ */ jsx10(
871
+ "div",
872
+ {
873
+ className: "rounded-md border border-border overflow-hidden bg-muted/20",
874
+ children: mediaType === "video" ? /* @__PURE__ */ jsx10("video", { src: url, controls: true, className: "w-full h-44 object-cover bg-black" }) : /* @__PURE__ */ jsx10(
875
+ "img",
876
+ {
877
+ src: url,
878
+ alt: `Generated media ${idx + 1}`,
879
+ className: "w-full h-44 object-cover",
880
+ loading: "lazy"
881
+ }
882
+ )
883
+ },
884
+ `${field.id}-${idx}`
885
+ );
886
+ }) }),
887
+ /* @__PURE__ */ jsxs2("div", { className: "space-y-1", children: [
888
+ /* @__PURE__ */ jsx10(Label2, { className: "text-xs font-medium", children: feedbackLabel }),
889
+ /* @__PURE__ */ jsx10(
890
+ Textarea,
891
+ {
892
+ value: mediaValue.feedback || "",
893
+ onChange: (e) => handleFieldChange(field.id, {
894
+ ...mediaValue,
895
+ feedback: e.target.value,
896
+ media_urls: mediaValue.media_urls ?? mediaUrls,
897
+ media_type: mediaValue.media_type ?? field.media_type ?? "mixed"
898
+ }),
899
+ placeholder: feedbackPlaceholder,
900
+ className: "bg-background min-h-[72px] w-full text-xs"
901
+ }
902
+ )
903
+ ] }),
904
+ /* @__PURE__ */ jsxs2("div", { className: "flex gap-2 justify-end", children: [
905
+ /* @__PURE__ */ jsxs2(
906
+ Button,
907
+ {
908
+ type: "button",
909
+ variant: "destructive",
910
+ size: "sm",
911
+ onClick: () => handleMediaApprovalDecision(field, "needs_modification"),
912
+ className: "bg-destructive/10 text-destructive hover:bg-destructive/20 border-transparent shadow-none h-8 text-xs",
913
+ disabled: isLoading,
914
+ children: [
915
+ /* @__PURE__ */ jsx10(AlertCircle, { className: "w-3 h-3 mr-1.5" }),
916
+ rejectLabel
917
+ ]
918
+ }
919
+ ),
920
+ /* @__PURE__ */ jsxs2(
921
+ Button,
922
+ {
923
+ type: "button",
924
+ size: "sm",
925
+ onClick: () => handleMediaApprovalDecision(field, "approved"),
926
+ className: "bg-emerald-600 hover:bg-emerald-700 text-white shadow-none border-transparent h-8 text-xs",
927
+ disabled: isLoading,
928
+ children: [
929
+ /* @__PURE__ */ jsx10(CheckCircle, { className: "w-3 h-3 mr-1.5" }),
930
+ approveLabel
931
+ ]
932
+ }
933
+ )
934
+ ] })
935
+ ] });
936
+ }
796
937
  case "todo_list": {
797
938
  const todoItems = formValues[field.id] || [];
798
939
  const completedCount = todoItems.filter((i) => i.status === "completed").length;
@@ -1095,7 +1236,7 @@ var dynamicFormManifest = {
1095
1236
  name: "common:dynamic-form",
1096
1237
  aliases: ["dynamic_form"],
1097
1238
  service: "common",
1098
- description: "Server-driven dynamic form supporting text, textarea, number, select, multiselect, checkbox, radio, approval, todo_list, and file upload fields",
1239
+ description: "Server-driven dynamic form supporting text, textarea, number, select, multiselect, checkbox, radio, approval, media_approval, todo_list, and file upload fields",
1099
1240
  containerStyle: {
1100
1241
  display: "flex",
1101
1242
  position: "relative",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamoid/chat-components",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
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",