@optifye/dashboard-core 6.9.9 → 6.9.12

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.js CHANGED
@@ -2925,8 +2925,8 @@ var AuthService = class {
2925
2925
  "Authorization": `Bearer ${accessToken}`,
2926
2926
  "Content-Type": "application/json"
2927
2927
  },
2928
- timeout: 3e5,
2929
- // 5 minutes
2928
+ timeout: 1e4,
2929
+ // 10 seconds
2930
2930
  retries: 1,
2931
2931
  silentErrors: false
2932
2932
  // We want to know about auth errors
@@ -2963,8 +2963,8 @@ var AuthService = class {
2963
2963
  "Authorization": `Bearer ${accessToken}`,
2964
2964
  "Content-Type": "application/json"
2965
2965
  },
2966
- timeout: 3e5,
2967
- // 5 minutes
2966
+ timeout: 1e4,
2967
+ // 10 seconds
2968
2968
  retries: 2,
2969
2969
  // More retries for validation
2970
2970
  silentErrors: true,
@@ -2996,8 +2996,8 @@ var AuthService = class {
2996
2996
  "Authorization": `Bearer ${accessToken}`,
2997
2997
  "Content-Type": "application/json"
2998
2998
  },
2999
- timeout: 3e5,
3000
- // 5 minutes
2999
+ timeout: 1e4,
3000
+ // 10 seconds
3001
3001
  retries: 1,
3002
3002
  silentErrors: false
3003
3003
  }
@@ -10912,7 +10912,7 @@ function useDateFormatter() {
10912
10912
  },
10913
10913
  [defaultTimezone, defaultLocale, dateFormatOptions]
10914
10914
  );
10915
- const formatTime3 = React23.useCallback(
10915
+ const formatTime4 = React23.useCallback(
10916
10916
  (date, formatString) => {
10917
10917
  const dateObj = typeof date === "string" ? dateFns.parseISO(date) : date;
10918
10918
  if (!dateFns.isValid(dateObj)) return "Invalid Time";
@@ -10943,7 +10943,7 @@ function useDateFormatter() {
10943
10943
  }, []);
10944
10944
  return {
10945
10945
  formatDate,
10946
- formatTime: formatTime3,
10946
+ formatTime: formatTime4,
10947
10947
  formatDateTime,
10948
10948
  getNow,
10949
10949
  timezone: defaultTimezone || "UTC",
@@ -23099,7 +23099,7 @@ var OutputProgressChartComponent = ({
23099
23099
  ];
23100
23100
  const COLORS = ["#00AB45", "#f3f4f6"];
23101
23101
  const percentage = (currentOutput / targetOutput * 100).toFixed(1);
23102
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `w-full h-full flex items-center justify-center ${className}`, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full aspect-square max-h-full", style: { maxWidth: "min(100%, 280px)" }, children: [
23102
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `w-full h-full flex items-center justify-center ${className}`, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full aspect-square max-h-full", style: { maxWidth: "min(100%, 280px)", containerType: "inline-size" }, children: [
23103
23103
  /* @__PURE__ */ jsxRuntime.jsx(recharts.ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxRuntime.jsx(recharts.PieChart, { children: /* @__PURE__ */ jsxRuntime.jsx(
23104
23104
  recharts.Pie,
23105
23105
  {
@@ -23123,16 +23123,33 @@ var OutputProgressChartComponent = ({
23123
23123
  ))
23124
23124
  }
23125
23125
  ) }) }),
23126
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 flex flex-col items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", children: [
23127
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-2xl sm:text-3xl lg:text-4xl font-bold text-gray-800", children: [
23128
- percentage,
23129
- "%"
23130
- ] }),
23131
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs sm:text-sm lg:text-base text-gray-500 mt-1 sm:mt-2", children: [
23132
- currentOutput,
23133
- " / ",
23134
- Math.round(targetOutput)
23135
- ] })
23126
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 flex flex-col items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", style: { width: "100%", padding: "0 10%" }, children: [
23127
+ /* @__PURE__ */ jsxRuntime.jsxs(
23128
+ "div",
23129
+ {
23130
+ className: "font-bold text-gray-800",
23131
+ style: { fontSize: "clamp(1.25rem, 8cqw, 2.5rem)" },
23132
+ children: [
23133
+ percentage,
23134
+ "%"
23135
+ ]
23136
+ }
23137
+ ),
23138
+ /* @__PURE__ */ jsxRuntime.jsxs(
23139
+ "div",
23140
+ {
23141
+ className: "text-gray-500",
23142
+ style: {
23143
+ fontSize: "clamp(0.7rem, 3.5cqw, 1rem)",
23144
+ marginTop: "clamp(0.125rem, 1cqw, 0.5rem)"
23145
+ },
23146
+ children: [
23147
+ currentOutput,
23148
+ " / ",
23149
+ Math.round(targetOutput)
23150
+ ]
23151
+ }
23152
+ )
23136
23153
  ] }) })
23137
23154
  ] }) });
23138
23155
  };
@@ -23150,7 +23167,7 @@ var LargeOutputProgressChart = ({
23150
23167
  ];
23151
23168
  const COLORS = ["#00AB45", "#f3f4f6"];
23152
23169
  const percentage = (currentOutput / targetOutput * 100).toFixed(1);
23153
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `w-full h-full flex items-center justify-center ${className}`, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full h-full", style: { minHeight: "100px", minWidth: "100px" }, children: [
23170
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `w-full h-full flex items-center justify-center ${className}`, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full h-full", style: { minHeight: "100px", minWidth: "100px", containerType: "inline-size" }, children: [
23154
23171
  /* @__PURE__ */ jsxRuntime.jsx(recharts.ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxRuntime.jsx(recharts.PieChart, { children: /* @__PURE__ */ jsxRuntime.jsx(
23155
23172
  recharts.Pie,
23156
23173
  {
@@ -23174,16 +23191,40 @@ var LargeOutputProgressChart = ({
23174
23191
  ))
23175
23192
  }
23176
23193
  ) }) }),
23177
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 flex flex-col items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", children: [
23178
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-4xl sm:text-5xl font-bold text-gray-900", children: currentOutput }),
23179
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-sm text-gray-500", children: [
23180
- "of ",
23181
- targetOutput
23182
- ] }),
23183
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-base font-medium text-gray-600 mt-1", children: [
23184
- percentage,
23185
- "%"
23186
- ] })
23194
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 flex flex-col items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", style: { width: "100%", padding: "0 10%" }, children: [
23195
+ /* @__PURE__ */ jsxRuntime.jsx(
23196
+ "div",
23197
+ {
23198
+ className: "font-bold text-gray-900",
23199
+ style: { fontSize: "clamp(1.5rem, 10cqw, 3rem)" },
23200
+ children: currentOutput
23201
+ }
23202
+ ),
23203
+ /* @__PURE__ */ jsxRuntime.jsxs(
23204
+ "div",
23205
+ {
23206
+ className: "text-gray-500",
23207
+ style: { fontSize: "clamp(0.75rem, 3.5cqw, 1rem)" },
23208
+ children: [
23209
+ "of ",
23210
+ targetOutput
23211
+ ]
23212
+ }
23213
+ ),
23214
+ /* @__PURE__ */ jsxRuntime.jsxs(
23215
+ "div",
23216
+ {
23217
+ className: "font-medium text-gray-600",
23218
+ style: {
23219
+ fontSize: "clamp(0.875rem, 4.5cqw, 1.25rem)",
23220
+ marginTop: "clamp(0.125rem, 1cqw, 0.5rem)"
23221
+ },
23222
+ children: [
23223
+ percentage,
23224
+ "%"
23225
+ ]
23226
+ }
23227
+ )
23187
23228
  ] }) })
23188
23229
  ] }) });
23189
23230
  };
@@ -23888,7 +23929,7 @@ var HourlyOutputChartComponent = ({
23888
23929
  endHour = Math.floor(endDecimalHour) % 24;
23889
23930
  endMinute = Math.round(endDecimalHour % 1 * 60);
23890
23931
  }
23891
- const formatTime3 = (h, m) => {
23932
+ const formatTime4 = (h, m) => {
23892
23933
  const period = h >= 12 ? "PM" : "AM";
23893
23934
  const hour12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
23894
23935
  if (m === 0) {
@@ -23896,7 +23937,7 @@ var HourlyOutputChartComponent = ({
23896
23937
  }
23897
23938
  return `${hour12}:${m.toString().padStart(2, "0")}${period}`;
23898
23939
  };
23899
- return `${formatTime3(startHour, startMinute)}-${formatTime3(endHour, endMinute)}`;
23940
+ return `${formatTime4(startHour, startMinute)}-${formatTime4(endHour, endMinute)}`;
23900
23941
  }, [shiftStartTime.decimalHour, SHIFT_DURATION, shiftEndTime]);
23901
23942
  const formatTimeRange = React23__namespace.default.useCallback((hourIndex) => {
23902
23943
  const isLastHour = hourIndex === SHIFT_DURATION - 1;
@@ -23912,12 +23953,12 @@ var HourlyOutputChartComponent = ({
23912
23953
  endHour = Math.floor(endDecimalHour) % 24;
23913
23954
  endMinute = Math.round(endDecimalHour % 1 * 60);
23914
23955
  }
23915
- const formatTime3 = (h, m) => {
23956
+ const formatTime4 = (h, m) => {
23916
23957
  const period = h >= 12 ? "PM" : "AM";
23917
23958
  const hour12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
23918
23959
  return `${hour12}:${m.toString().padStart(2, "0")} ${period}`;
23919
23960
  };
23920
- return `${formatTime3(startHour, startMinute)} - ${formatTime3(endHour, endMinute)}`;
23961
+ return `${formatTime4(startHour, startMinute)} - ${formatTime4(endHour, endMinute)}`;
23921
23962
  }, [shiftStartTime.decimalHour, SHIFT_DURATION, shiftEndTime]);
23922
23963
  const chartData = React23__namespace.default.useMemo(() => {
23923
23964
  return Array.from({ length: SHIFT_DURATION }, (_, i) => {
@@ -25088,7 +25129,7 @@ var SOPComplianceChart = ({
25088
25129
  }
25089
25130
  };
25090
25131
  }, [data, animateToNewData, mockData]);
25091
- const formatTime3 = (minuteIndex) => {
25132
+ const formatTime4 = (minuteIndex) => {
25092
25133
  const totalMinutes = shiftStartHour * 60 + minuteIndex;
25093
25134
  const hours = Math.floor(totalMinutes / 60) % 24;
25094
25135
  const minutes = totalMinutes % 60;
@@ -25100,7 +25141,7 @@ var SOPComplianceChart = ({
25100
25141
  const hasDataForMinute = index < animatedData.length - 10;
25101
25142
  return {
25102
25143
  minute: index,
25103
- time: formatTime3(index),
25144
+ time: formatTime4(index),
25104
25145
  compliance: hasDataForMinute ? animatedData[index] : null
25105
25146
  };
25106
25147
  });
@@ -25277,7 +25318,7 @@ var GaugeChart = ({
25277
25318
  };
25278
25319
  const gaugeColor = getColor();
25279
25320
  const targetAngle = target !== void 0 ? 180 - (target - min) / (max - min) * 180 : null;
25280
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `relative w-full h-full flex flex-col items-center justify-center ${className}`, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full max-w-[280px] aspect-square", style: { minHeight: "100px", minWidth: "100px" }, children: [
25321
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `relative w-full h-full flex flex-col items-center justify-center ${className}`, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative w-full max-w-[280px] aspect-square", style: { minHeight: "100px", minWidth: "100px", containerType: "inline-size" }, children: [
25281
25322
  /* @__PURE__ */ jsxRuntime.jsx(recharts.ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxRuntime.jsx(recharts.PieChart, { children: /* @__PURE__ */ jsxRuntime.jsxs(
25282
25323
  recharts.Pie,
25283
25324
  {
@@ -25310,17 +25351,44 @@ var GaugeChart = ({
25310
25351
  children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute -top-1 -left-1.5 w-3 h-3 bg-gray-800 rounded-full" })
25311
25352
  }
25312
25353
  ),
25313
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 flex flex-col items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", children: [
25314
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-3xl font-bold text-gray-800", children: [
25315
- value.toFixed(unit === "%" ? 1 : 0),
25316
- unit
25317
- ] }),
25318
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm text-gray-600 mt-1 font-medium", children: label }),
25319
- target !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs text-gray-500 mt-1", children: [
25320
- "Target: ",
25321
- target,
25322
- unit
25323
- ] })
25354
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 flex flex-col items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", style: { width: "100%", padding: "0 10%" }, children: [
25355
+ /* @__PURE__ */ jsxRuntime.jsxs(
25356
+ "div",
25357
+ {
25358
+ className: "font-bold text-gray-800",
25359
+ style: { fontSize: "clamp(1.25rem, 8cqw, 2rem)" },
25360
+ children: [
25361
+ value.toFixed(unit === "%" ? 1 : 0),
25362
+ unit
25363
+ ]
25364
+ }
25365
+ ),
25366
+ /* @__PURE__ */ jsxRuntime.jsx(
25367
+ "div",
25368
+ {
25369
+ className: "text-gray-600 font-medium",
25370
+ style: {
25371
+ fontSize: "clamp(0.75rem, 3.5cqw, 1rem)",
25372
+ marginTop: "clamp(0.125rem, 1cqw, 0.25rem)"
25373
+ },
25374
+ children: label
25375
+ }
25376
+ ),
25377
+ target !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs(
25378
+ "div",
25379
+ {
25380
+ className: "text-gray-500",
25381
+ style: {
25382
+ fontSize: "clamp(0.7rem, 3cqw, 0.875rem)",
25383
+ marginTop: "clamp(0.125rem, 1cqw, 0.25rem)"
25384
+ },
25385
+ children: [
25386
+ "Target: ",
25387
+ target,
25388
+ unit
25389
+ ]
25390
+ }
25391
+ )
25324
25392
  ] }) }),
25325
25393
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute bottom-[15%] left-[15%] text-xs text-gray-500", children: [
25326
25394
  min,
@@ -25507,7 +25575,7 @@ var DateTimeDisplay = ({
25507
25575
  const {
25508
25576
  defaultTimezone
25509
25577
  } = useDateTimeConfig();
25510
- const { formatDate, formatTime: formatTime3 } = useDateFormatter();
25578
+ const { formatDate, formatTime: formatTime4 } = useDateFormatter();
25511
25579
  const [now2, setNow] = React23.useState(() => getCurrentTimeInZone(defaultTimezone || "UTC"));
25512
25580
  React23.useEffect(() => {
25513
25581
  const timerId = setInterval(() => {
@@ -25519,7 +25587,7 @@ var DateTimeDisplay = ({
25519
25587
  return null;
25520
25588
  }
25521
25589
  const formattedDate = showDate ? formatDate(now2) : "";
25522
- const formattedTime = showTime ? formatTime3(now2) : "";
25590
+ const formattedTime = showTime ? formatTime4(now2) : "";
25523
25591
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: clsx_default("flex items-center space-x-2 text-sm text-gray-700 dark:text-gray-300", className), children: [
25524
25592
  showDate && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "date-display", "aria-label": `Current date: ${formattedDate}`, children: formattedDate }),
25525
25593
  showDate && showTime && formattedDate && formattedTime && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "separator", "aria-hidden": "true", children: "|" }),
@@ -25683,7 +25751,7 @@ var BreakNotificationPopup = ({
25683
25751
  const handlePrevious = () => {
25684
25752
  setCurrentIndex((prev) => (prev - 1 + visibleBreaks.length) % visibleBreaks.length);
25685
25753
  };
25686
- const formatTime3 = (minutes) => {
25754
+ const formatTime4 = (minutes) => {
25687
25755
  const hours = Math.floor(minutes / 60);
25688
25756
  const mins = minutes % 60;
25689
25757
  if (hours > 0) {
@@ -25757,9 +25825,9 @@ var BreakNotificationPopup = ({
25757
25825
  formatTo12Hour(currentBreak.endTime)
25758
25826
  ] }),
25759
25827
  /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-gray-500 mb-2", children: [
25760
- formatTime3(currentBreak.elapsedMinutes),
25828
+ formatTime4(currentBreak.elapsedMinutes),
25761
25829
  " elapsed of ",
25762
- formatTime3(currentBreak.duration),
25830
+ formatTime4(currentBreak.duration),
25763
25831
  " total"
25764
25832
  ] }),
25765
25833
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full bg-gray-200 rounded-full h-1.5", children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -26063,64 +26131,208 @@ var getSeverityColor = (severity) => {
26063
26131
  return "bg-gray-500";
26064
26132
  }
26065
26133
  };
26066
- var PlayPauseIndicator = ({
26067
- show,
26134
+ var formatTime2 = (seconds) => {
26135
+ if (!seconds || isNaN(seconds)) return "0:00";
26136
+ const h = Math.floor(seconds / 3600);
26137
+ const m = Math.floor(seconds % 3600 / 60);
26138
+ const s = Math.floor(seconds % 60);
26139
+ if (h > 0) {
26140
+ return `${h}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
26141
+ }
26142
+ return `${m}:${s.toString().padStart(2, "0")}`;
26143
+ };
26144
+ var VideoControls = ({
26068
26145
  isPlaying,
26069
- duration = 600
26146
+ currentTime,
26147
+ duration,
26148
+ buffered,
26149
+ showControls,
26150
+ controlsPinned = false,
26151
+ onTogglePinControls,
26152
+ playbackRate = 1,
26153
+ onPlayPause,
26154
+ onSeek,
26155
+ onSeekStart,
26156
+ onSeekEnd,
26157
+ onToggleFullscreen,
26158
+ onPlaybackRateChange,
26159
+ className = ""
26070
26160
  }) => {
26071
- const [isVisible, setIsVisible] = React23.useState(false);
26072
- const [isFading, setIsFading] = React23.useState(false);
26161
+ const [isDragging, setIsDragging] = React23.useState(false);
26162
+ const [dragTime, setDragTime] = React23.useState(0);
26163
+ const [isHoveringProgressBar, setIsHoveringProgressBar] = React23.useState(false);
26164
+ const [showSpeedMenu, setShowSpeedMenu] = React23.useState(false);
26165
+ const speedMenuRef = React23.useRef(null);
26166
+ const progressColor = "#4b5563";
26167
+ const controlsVisible = showControls || controlsPinned;
26168
+ const getPercentage = (current, total) => {
26169
+ if (!total || total === 0) return 0;
26170
+ return Math.min(Math.max(current / total * 100, 0), 100);
26171
+ };
26172
+ const handleSeekChange = (e) => {
26173
+ const newTime = parseFloat(e.target.value);
26174
+ setDragTime(newTime);
26175
+ onSeek(newTime);
26176
+ };
26177
+ const handleSeekStart = () => {
26178
+ setIsDragging(true);
26179
+ setDragTime(currentTime);
26180
+ onSeekStart?.();
26181
+ };
26182
+ const handleSeekEnd = () => {
26183
+ setIsDragging(false);
26184
+ onSeekEnd?.();
26185
+ };
26073
26186
  React23.useEffect(() => {
26074
- if (show) {
26075
- setIsVisible(true);
26076
- setIsFading(false);
26077
- const fadeTimer = setTimeout(() => {
26078
- setIsFading(true);
26079
- }, 100);
26080
- const hideTimer = setTimeout(() => {
26081
- setIsVisible(false);
26082
- setIsFading(false);
26083
- }, duration);
26084
- return () => {
26085
- clearTimeout(fadeTimer);
26086
- clearTimeout(hideTimer);
26087
- };
26088
- }
26089
- }, [show, duration]);
26090
- if (!isVisible) return null;
26091
- return /* @__PURE__ */ jsxRuntime.jsx(
26187
+ const handleClickOutside = (event) => {
26188
+ if (speedMenuRef.current && !speedMenuRef.current.contains(event.target)) {
26189
+ setShowSpeedMenu(false);
26190
+ }
26191
+ };
26192
+ document.addEventListener("mousedown", handleClickOutside);
26193
+ return () => {
26194
+ document.removeEventListener("mousedown", handleClickOutside);
26195
+ };
26196
+ }, []);
26197
+ const displayTime = isDragging ? dragTime : currentTime;
26198
+ const progressPercent = getPercentage(displayTime, duration);
26199
+ const bufferedPercent = getPercentage(buffered, duration);
26200
+ return /* @__PURE__ */ jsxRuntime.jsxs(
26092
26201
  "div",
26093
26202
  {
26094
- className: "absolute inset-0 flex items-center justify-center pointer-events-none z-10",
26095
- style: {
26096
- opacity: isFading ? 0 : 1,
26097
- transition: `opacity ${duration - 100}ms ease-out`
26098
- },
26099
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-black/70 rounded-full p-6", children: isPlaying ? (
26100
- // Play icon (triangle)
26101
- /* @__PURE__ */ jsxRuntime.jsx(
26102
- "svg",
26103
- {
26104
- xmlns: "http://www.w3.org/2000/svg",
26105
- viewBox: "0 0 24 24",
26106
- fill: "white",
26107
- className: "w-16 h-16",
26108
- children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 5v14l11-7z" })
26109
- }
26110
- )
26111
- ) : (
26112
- // Pause icon (two bars)
26113
- /* @__PURE__ */ jsxRuntime.jsx(
26114
- "svg",
26203
+ className: `absolute bottom-0 left-0 right-0 px-3 pb-3 pt-12 bg-gradient-to-t from-black/80 via-black/40 to-transparent transition-opacity duration-300 ${controlsVisible ? "opacity-100" : "opacity-0 pointer-events-none"} ${className}`,
26204
+ style: { touchAction: "none" },
26205
+ children: [
26206
+ /* @__PURE__ */ jsxRuntime.jsxs(
26207
+ "div",
26115
26208
  {
26116
- xmlns: "http://www.w3.org/2000/svg",
26117
- viewBox: "0 0 24 24",
26118
- fill: "white",
26119
- className: "w-16 h-16",
26120
- children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 4h4v16H6V4zm8 0h4v16h-4V4z" })
26209
+ className: "relative h-1 mb-4 group cursor-pointer",
26210
+ onMouseEnter: () => setIsHoveringProgressBar(true),
26211
+ onMouseLeave: () => setIsHoveringProgressBar(false),
26212
+ children: [
26213
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute -top-2 -bottom-2 left-0 right-0 z-20" }),
26214
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute top-0 left-0 right-0 bottom-0 bg-white/20 rounded-full overflow-hidden z-0", children: /* @__PURE__ */ jsxRuntime.jsx(
26215
+ "div",
26216
+ {
26217
+ className: "absolute top-0 left-0 bottom-0 bg-white/40 transition-all duration-200",
26218
+ style: { width: `${bufferedPercent}%` }
26219
+ }
26220
+ ) }),
26221
+ /* @__PURE__ */ jsxRuntime.jsx(
26222
+ "div",
26223
+ {
26224
+ className: "absolute top-0 left-0 bottom-0 bg-[#007bff] transition-all duration-75 z-10",
26225
+ style: { width: `${progressPercent}%`, backgroundColor: progressColor },
26226
+ children: /* @__PURE__ */ jsxRuntime.jsx(
26227
+ "div",
26228
+ {
26229
+ className: `absolute right-0 top-1/2 -translate-y-1/2 translate-x-1/2 w-3 h-3 rounded-full shadow transform transition-transform duration-200 ${isHoveringProgressBar || isDragging ? "scale-100" : "scale-0"}`,
26230
+ style: { backgroundColor: progressColor }
26231
+ }
26232
+ )
26233
+ }
26234
+ ),
26235
+ /* @__PURE__ */ jsxRuntime.jsx(
26236
+ "input",
26237
+ {
26238
+ type: "range",
26239
+ min: "0",
26240
+ max: duration || 100,
26241
+ step: "0.1",
26242
+ value: displayTime,
26243
+ onChange: handleSeekChange,
26244
+ onMouseDown: handleSeekStart,
26245
+ onMouseUp: handleSeekEnd,
26246
+ onTouchStart: handleSeekStart,
26247
+ onTouchEnd: handleSeekEnd,
26248
+ className: "absolute inset-0 w-full h-full opacity-0 cursor-pointer z-30 margin-0 padding-0"
26249
+ }
26250
+ )
26251
+ ]
26121
26252
  }
26122
- )
26123
- ) })
26253
+ ),
26254
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-white", children: [
26255
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4", children: [
26256
+ /* @__PURE__ */ jsxRuntime.jsx(
26257
+ "button",
26258
+ {
26259
+ onClick: (e) => {
26260
+ e.stopPropagation();
26261
+ onPlayPause();
26262
+ },
26263
+ className: "hover:text-[#007bff] transition-colors focus:outline-none",
26264
+ "aria-label": isPlaying ? "Pause" : "Play",
26265
+ children: isPlaying ? /* @__PURE__ */ jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 4h4v16H6V4zm8 0h4v16h-4V4z" }) }) : /* @__PURE__ */ jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 5v14l11-7z" }) })
26266
+ }
26267
+ ),
26268
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs font-medium font-sans", children: [
26269
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: formatTime2(displayTime) }),
26270
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mx-1 text-white/70", children: "/" }),
26271
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-white/70", children: formatTime2(duration) })
26272
+ ] })
26273
+ ] }),
26274
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4", children: [
26275
+ onTogglePinControls && /* @__PURE__ */ jsxRuntime.jsx(
26276
+ "button",
26277
+ {
26278
+ onClick: (e) => {
26279
+ e.stopPropagation();
26280
+ onTogglePinControls();
26281
+ },
26282
+ className: `transition-colors focus:outline-none ${controlsPinned ? "text-[#007bff]" : "hover:text-[#007bff]"}`,
26283
+ "aria-label": controlsPinned ? "Unpin controls" : "Pin controls",
26284
+ title: controlsPinned ? "Unpin controls" : "Pin controls",
26285
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "currentColor", children: controlsPinned ? /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 3h6l-1 7h3v2h-4.5l-.5 4.5-2 1L10 12H6v-2h3z" }) : /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 3h6l-1 7h3v2h-4v5l-2 1-1-6H6v-2h3z" }) })
26286
+ }
26287
+ ),
26288
+ onPlaybackRateChange && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", ref: speedMenuRef, children: [
26289
+ /* @__PURE__ */ jsxRuntime.jsxs(
26290
+ "button",
26291
+ {
26292
+ onClick: (e) => {
26293
+ e.stopPropagation();
26294
+ setShowSpeedMenu(!showSpeedMenu);
26295
+ },
26296
+ className: "text-xs font-medium hover:text-[#007bff] transition-colors focus:outline-none min-w-[32px]",
26297
+ "aria-label": "Playback Speed",
26298
+ children: [
26299
+ playbackRate,
26300
+ "x"
26301
+ ]
26302
+ }
26303
+ ),
26304
+ showSpeedMenu && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute bottom-full right-0 mb-2 bg-black/90 text-white rounded shadow-lg overflow-hidden z-50 min-w-[80px]", children: [0.5, 1, 1.5, 2, 2.5, 3, 4, 5].map((rate) => /* @__PURE__ */ jsxRuntime.jsxs(
26305
+ "button",
26306
+ {
26307
+ onClick: (e) => {
26308
+ e.stopPropagation();
26309
+ onPlaybackRateChange(rate);
26310
+ setShowSpeedMenu(false);
26311
+ },
26312
+ className: `block w-full text-left px-4 py-2 text-xs hover:bg-white/20 transition-colors ${playbackRate === rate ? "text-[#007bff] font-bold" : ""}`,
26313
+ children: [
26314
+ rate,
26315
+ "x"
26316
+ ]
26317
+ },
26318
+ rate
26319
+ )) })
26320
+ ] }),
26321
+ onToggleFullscreen && /* @__PURE__ */ jsxRuntime.jsx(
26322
+ "button",
26323
+ {
26324
+ onClick: (e) => {
26325
+ e.stopPropagation();
26326
+ onToggleFullscreen();
26327
+ },
26328
+ className: "hover:text-[#007bff] transition-colors focus:outline-none",
26329
+ "aria-label": "Toggle Fullscreen",
26330
+ children: /* @__PURE__ */ jsxRuntime.jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" }) })
26331
+ }
26332
+ )
26333
+ ] })
26334
+ ] })
26335
+ ]
26124
26336
  }
26125
26337
  );
26126
26338
  };
@@ -26248,9 +26460,15 @@ var HlsVideoPlayer = React23.forwardRef(({
26248
26460
  const blobUrlRef = React23.useRef(null);
26249
26461
  const [isReady, setIsReady] = React23.useState(false);
26250
26462
  const [isLoading, setIsLoading] = React23.useState(true);
26251
- const [showIndicator, setShowIndicator] = React23.useState(false);
26252
- const [indicatorIsPlaying, setIndicatorIsPlaying] = React23.useState(false);
26253
- const indicatorKeyRef = React23.useRef(0);
26463
+ const [showControls, setShowControls] = React23.useState(true);
26464
+ const [controlsPinned, setControlsPinned] = React23.useState(false);
26465
+ const [isPlaying, setIsPlaying] = React23.useState(false);
26466
+ const [currentTime, setCurrentTime] = React23.useState(0);
26467
+ const [duration, setDuration] = React23.useState(0);
26468
+ const [buffered, setBuffered] = React23.useState(0);
26469
+ const [playbackRate, setPlaybackRate] = React23.useState(1);
26470
+ const userSeekingRef = React23.useRef(false);
26471
+ const controlsTimeoutRef = React23.useRef(null);
26254
26472
  const eventCallbacksRef = React23.useRef({
26255
26473
  onReady,
26256
26474
  onPlay,
@@ -26429,8 +26647,6 @@ var HlsVideoPlayer = React23.forwardRef(({
26429
26647
  }
26430
26648
  });
26431
26649
  hls.on(Hls3.Events.FRAG_LOADING, () => {
26432
- setIsLoading(true);
26433
- eventCallbacksRef.current.onLoadingChange?.(true);
26434
26650
  });
26435
26651
  hls.on(Hls3.Events.FRAG_LOADED, () => {
26436
26652
  setIsLoading(false);
@@ -26484,25 +26700,52 @@ var HlsVideoPlayer = React23.forwardRef(({
26484
26700
  const handleCanPlay = () => {
26485
26701
  if (!hlsRef.current) {
26486
26702
  setIsReady(true);
26487
- onReady?.(player);
26488
26703
  }
26704
+ setIsLoading(false);
26705
+ eventCallbacksRef.current.onLoadingChange?.(false);
26706
+ onReady?.(player);
26707
+ };
26708
+ const handlePlay = () => {
26709
+ setIsPlaying(true);
26710
+ eventCallbacksRef.current.onPlay?.(player);
26711
+ };
26712
+ const handlePause = () => {
26713
+ if (userSeekingRef.current && videoRef.current) {
26714
+ videoRef.current.play().catch((err) => console.warn("Auto-resume after seek pause failed:", err));
26715
+ return;
26716
+ }
26717
+ setIsPlaying(false);
26718
+ eventCallbacksRef.current.onPause?.(player);
26489
26719
  };
26490
- const handlePlay = () => eventCallbacksRef.current.onPlay?.(player);
26491
- const handlePause = () => eventCallbacksRef.current.onPause?.(player);
26492
26720
  const handlePlaying = () => {
26493
26721
  setIsLoading(false);
26722
+ setIsPlaying(true);
26494
26723
  eventCallbacksRef.current.onLoadingChange?.(false);
26495
26724
  eventCallbacksRef.current.onPlaying?.(player);
26496
26725
  };
26497
26726
  const handleTimeUpdate = () => {
26498
26727
  const currentTime2 = video.currentTime || 0;
26728
+ setCurrentTime(currentTime2);
26729
+ if (video.buffered.length > 0) {
26730
+ for (let i = 0; i < video.buffered.length; i++) {
26731
+ if (video.buffered.start(i) <= currentTime2 && video.buffered.end(i) >= currentTime2) {
26732
+ setBuffered(video.buffered.end(i));
26733
+ break;
26734
+ }
26735
+ }
26736
+ }
26499
26737
  eventCallbacksRef.current.onTimeUpdate?.(player, currentTime2);
26500
26738
  };
26501
26739
  const handleDurationChange = () => {
26502
26740
  const duration2 = video.duration || 0;
26741
+ setDuration(duration2);
26503
26742
  eventCallbacksRef.current.onDurationChange?.(player, duration2);
26504
26743
  };
26505
- const handleEnded = () => eventCallbacksRef.current.onEnded?.(player);
26744
+ const handleEnded = () => {
26745
+ setIsPlaying(false);
26746
+ userSeekingRef.current = false;
26747
+ eventCallbacksRef.current.onEnded?.(player);
26748
+ };
26506
26749
  const handleLoadStart = () => {
26507
26750
  setIsLoading(true);
26508
26751
  eventCallbacksRef.current.onLoadingChange?.(true);
@@ -26518,8 +26761,19 @@ var HlsVideoPlayer = React23.forwardRef(({
26518
26761
  setIsLoading(true);
26519
26762
  eventCallbacksRef.current.onLoadingChange?.(true);
26520
26763
  };
26521
- const handleSeeking = () => eventCallbacksRef.current.onSeeking?.(player);
26522
- const handleSeeked = () => eventCallbacksRef.current.onSeeked?.(player);
26764
+ const handleSeeking = () => {
26765
+ userSeekingRef.current = true;
26766
+ eventCallbacksRef.current.onSeeking?.(player);
26767
+ };
26768
+ const handleSeeked = () => {
26769
+ setIsLoading(false);
26770
+ eventCallbacksRef.current.onLoadingChange?.(false);
26771
+ if (videoRef.current) {
26772
+ videoRef.current.play().catch((err) => console.warn("Resume playback after seek failed:", err));
26773
+ }
26774
+ userSeekingRef.current = false;
26775
+ eventCallbacksRef.current.onSeeked?.(player);
26776
+ };
26523
26777
  const handleError = () => {
26524
26778
  const error = video.error;
26525
26779
  if (error) {
@@ -26532,6 +26786,10 @@ var HlsVideoPlayer = React23.forwardRef(({
26532
26786
  eventCallbacksRef.current.onError?.(player, errorInfo);
26533
26787
  }
26534
26788
  };
26789
+ const handlePlaybackRateChange2 = (e) => {
26790
+ const target = e.target;
26791
+ setPlaybackRate(target.playbackRate);
26792
+ };
26535
26793
  video.addEventListener("canplay", handleCanPlay);
26536
26794
  video.addEventListener("play", handlePlay);
26537
26795
  video.addEventListener("pause", handlePause);
@@ -26546,6 +26804,7 @@ var HlsVideoPlayer = React23.forwardRef(({
26546
26804
  video.addEventListener("seeking", handleSeeking);
26547
26805
  video.addEventListener("seeked", handleSeeked);
26548
26806
  video.addEventListener("error", handleError);
26807
+ video.addEventListener("ratechange", handlePlaybackRateChange2);
26549
26808
  return () => {
26550
26809
  video.removeEventListener("canplay", handleCanPlay);
26551
26810
  video.removeEventListener("play", handlePlay);
@@ -26561,6 +26820,7 @@ var HlsVideoPlayer = React23.forwardRef(({
26561
26820
  video.removeEventListener("seeking", handleSeeking);
26562
26821
  video.removeEventListener("seeked", handleSeeked);
26563
26822
  video.removeEventListener("error", handleError);
26823
+ video.removeEventListener("ratechange", handlePlaybackRateChange2);
26564
26824
  };
26565
26825
  }, [
26566
26826
  src,
@@ -26589,20 +26849,46 @@ var HlsVideoPlayer = React23.forwardRef(({
26589
26849
  }
26590
26850
  }
26591
26851
  }, [autoplay]);
26852
+ const resetControlsTimeout = React23.useCallback(() => {
26853
+ if (controlsPinned) {
26854
+ setShowControls(true);
26855
+ return;
26856
+ }
26857
+ setShowControls(true);
26858
+ if (controlsTimeoutRef.current) {
26859
+ clearTimeout(controlsTimeoutRef.current);
26860
+ }
26861
+ if (isPlaying) {
26862
+ controlsTimeoutRef.current = setTimeout(() => {
26863
+ setShowControls(false);
26864
+ }, 3e3);
26865
+ }
26866
+ }, [isPlaying, controlsPinned]);
26867
+ const handleMouseMove = React23.useCallback(() => {
26868
+ resetControlsTimeout();
26869
+ }, [resetControlsTimeout]);
26870
+ React23.useEffect(() => {
26871
+ resetControlsTimeout();
26872
+ return () => {
26873
+ if (controlsTimeoutRef.current) {
26874
+ clearTimeout(controlsTimeoutRef.current);
26875
+ }
26876
+ };
26877
+ }, [isPlaying, resetControlsTimeout]);
26592
26878
  const play = React23.useCallback(() => {
26593
26879
  return videoRef.current?.play();
26594
26880
  }, []);
26595
26881
  const pause = React23.useCallback(() => {
26596
26882
  videoRef.current?.pause();
26597
26883
  }, []);
26598
- const currentTime = React23.useCallback((time2) => {
26884
+ const currentTimeProp = React23.useCallback((time2) => {
26599
26885
  if (time2 !== void 0 && videoRef.current) {
26600
26886
  videoRef.current.currentTime = time2;
26601
26887
  return time2;
26602
26888
  }
26603
26889
  return videoRef.current?.currentTime || 0;
26604
26890
  }, []);
26605
- const duration = React23.useCallback(() => {
26891
+ const durationProp = React23.useCallback(() => {
26606
26892
  return videoRef.current?.duration || 0;
26607
26893
  }, []);
26608
26894
  const paused = React23.useCallback(() => {
@@ -26615,95 +26901,144 @@ var HlsVideoPlayer = React23.forwardRef(({
26615
26901
  }
26616
26902
  return videoRef.current?.muted ?? false;
26617
26903
  }, []);
26618
- const volume = React23.useCallback((level) => {
26904
+ const volumeProp = React23.useCallback((level) => {
26619
26905
  if (level !== void 0 && videoRef.current) {
26620
26906
  videoRef.current.volume = level;
26621
26907
  return level;
26622
26908
  }
26623
26909
  return videoRef.current?.volume ?? 1;
26624
26910
  }, []);
26625
- const playbackRate = React23.useCallback((rate) => {
26911
+ const playbackRateProp = React23.useCallback((rate) => {
26626
26912
  if (rate !== void 0 && videoRef.current) {
26627
26913
  videoRef.current.playbackRate = rate;
26628
26914
  return rate;
26629
26915
  }
26630
26916
  return videoRef.current?.playbackRate ?? 1;
26631
26917
  }, []);
26918
+ const handleTogglePlay = React23.useCallback(() => {
26919
+ if (videoRef.current) {
26920
+ if (videoRef.current.paused) {
26921
+ videoRef.current.play();
26922
+ } else {
26923
+ videoRef.current.pause();
26924
+ }
26925
+ }
26926
+ }, []);
26927
+ const handleSeek = React23.useCallback((time2) => {
26928
+ if (videoRef.current) {
26929
+ videoRef.current.currentTime = time2;
26930
+ videoRef.current.play().catch((err) => console.warn("Resume playback failed during seek:", err));
26931
+ }
26932
+ }, []);
26933
+ const handleSeekStart = React23.useCallback(() => {
26934
+ userSeekingRef.current = true;
26935
+ }, []);
26936
+ const handleSeekEnd = React23.useCallback(() => {
26937
+ if (videoRef.current) {
26938
+ videoRef.current.play().catch((err) => console.warn("Resume playback failed after seek:", err));
26939
+ }
26940
+ }, []);
26941
+ const handlePlaybackRateChange = React23.useCallback((rate) => {
26942
+ if (videoRef.current) {
26943
+ videoRef.current.playbackRate = rate;
26944
+ }
26945
+ }, []);
26946
+ const handleToggleFullscreen = React23.useCallback(() => {
26947
+ if (videoContainerRef.current) {
26948
+ if (!document.fullscreenElement) {
26949
+ videoContainerRef.current.requestFullscreen();
26950
+ } else {
26951
+ document.exitFullscreen();
26952
+ }
26953
+ }
26954
+ }, []);
26632
26955
  React23.useImperativeHandle(ref, () => ({
26633
26956
  hls: hlsRef.current,
26634
26957
  video: videoRef.current,
26635
26958
  play,
26636
26959
  pause,
26637
- currentTime,
26638
- duration,
26960
+ currentTime: currentTimeProp,
26961
+ duration: durationProp,
26639
26962
  paused,
26640
26963
  mute,
26641
- volume,
26642
- playbackRate,
26964
+ volume: volumeProp,
26965
+ playbackRate: playbackRateProp,
26643
26966
  dispose,
26644
26967
  isReady,
26645
26968
  // For backward compatibility with Video.js API
26646
26969
  player: playerLikeObject()
26647
- }), [play, pause, currentTime, duration, paused, mute, volume, playbackRate, dispose, isReady, playerLikeObject]);
26648
- const handleClickWithIndicator = React23.useCallback(() => {
26649
- if (!onClick || !videoRef.current) return;
26650
- const willBePlaying = videoRef.current.paused;
26651
- setIndicatorIsPlaying(willBePlaying);
26652
- setShowIndicator(false);
26653
- setTimeout(() => {
26654
- indicatorKeyRef.current += 1;
26655
- setShowIndicator(true);
26656
- }, 0);
26657
- onClick();
26658
- }, [onClick]);
26659
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `hls-video-player-wrapper ${className}`, style: { position: "relative", width: "100%", height: "100%" }, children: [
26660
- /* @__PURE__ */ jsxRuntime.jsx(
26661
- "div",
26662
- {
26663
- className: "hls-video-player-container",
26664
- ref: videoContainerRef,
26665
- children: /* @__PURE__ */ jsxRuntime.jsx(
26666
- "video",
26970
+ }), [play, pause, currentTimeProp, durationProp, paused, mute, volumeProp, playbackRateProp, dispose, isReady, playerLikeObject]);
26971
+ const handleContainerClick = React23.useCallback(() => {
26972
+ if (!onClick && !controls) {
26973
+ handleTogglePlay();
26974
+ }
26975
+ if (onClick) {
26976
+ onClick();
26977
+ }
26978
+ }, [onClick, controls, handleTogglePlay]);
26979
+ return /* @__PURE__ */ jsxRuntime.jsxs(
26980
+ "div",
26981
+ {
26982
+ className: `hls-video-player-wrapper ${className} group`,
26983
+ style: { position: "relative", width: "100%", height: "100%" },
26984
+ onMouseMove: handleMouseMove,
26985
+ onMouseLeave: () => isPlaying && !controlsPinned && setShowControls(false),
26986
+ children: [
26987
+ /* @__PURE__ */ jsxRuntime.jsxs(
26988
+ "div",
26667
26989
  {
26668
- ref: videoRef,
26669
- className: "hls-video-element",
26670
- poster,
26671
- controls,
26672
- loop,
26673
- muted,
26674
- playsInline,
26675
- autoPlay: autoplay,
26676
- preload: "metadata"
26990
+ className: "hls-video-player-container",
26991
+ ref: videoContainerRef,
26992
+ onClick: handleContainerClick,
26993
+ children: [
26994
+ /* @__PURE__ */ jsxRuntime.jsx(
26995
+ "video",
26996
+ {
26997
+ ref: videoRef,
26998
+ className: "hls-video-element",
26999
+ poster,
27000
+ controls: false,
27001
+ loop,
27002
+ muted,
27003
+ playsInline,
27004
+ autoPlay: autoplay,
27005
+ preload: "metadata"
27006
+ }
27007
+ ),
27008
+ controls && /* @__PURE__ */ jsxRuntime.jsx(
27009
+ VideoControls,
27010
+ {
27011
+ isPlaying,
27012
+ currentTime,
27013
+ duration,
27014
+ buffered,
27015
+ showControls: controlsPinned || showControls || !isPlaying,
27016
+ controlsPinned,
27017
+ playbackRate,
27018
+ onPlayPause: handleTogglePlay,
27019
+ onSeek: handleSeek,
27020
+ onSeekStart: handleSeekStart,
27021
+ onSeekEnd: handleSeekEnd,
27022
+ onPlaybackRateChange: handlePlaybackRateChange,
27023
+ onTogglePinControls: () => setControlsPinned((prev) => {
27024
+ const next = !prev;
27025
+ if (next) {
27026
+ setShowControls(true);
27027
+ } else {
27028
+ resetControlsTimeout();
27029
+ }
27030
+ return next;
27031
+ }),
27032
+ onToggleFullscreen: handleToggleFullscreen
27033
+ }
27034
+ )
27035
+ ]
26677
27036
  }
26678
- )
26679
- }
26680
- ),
26681
- isLoading && !externalLoadingControl && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hls-video-player-loading", children: /* @__PURE__ */ jsxRuntime.jsx(OptifyeLogoLoader_default, { size: "md", message: "Loading video..." }) }),
26682
- onClick && !controls && /* @__PURE__ */ jsxRuntime.jsx(
26683
- "div",
26684
- {
26685
- onClick: handleClickWithIndicator,
26686
- style: {
26687
- position: "absolute",
26688
- top: 0,
26689
- left: 0,
26690
- right: 0,
26691
- bottom: 0,
26692
- zIndex: 1,
26693
- cursor: "pointer"
26694
- },
26695
- "aria-label": "Click to play/pause"
26696
- }
26697
- ),
26698
- onClick && !controls && /* @__PURE__ */ jsxRuntime.jsx(
26699
- PlayPauseIndicator,
26700
- {
26701
- show: showIndicator,
26702
- isPlaying: indicatorIsPlaying
26703
- },
26704
- indicatorKeyRef.current
26705
- )
26706
- ] });
27037
+ ),
27038
+ isLoading && !externalLoadingControl && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hls-video-player-loading", children: /* @__PURE__ */ jsxRuntime.jsx(OptifyeLogoLoader_default, { size: "md", message: "Loading video..." }) })
27039
+ ]
27040
+ }
27041
+ );
26707
27042
  });
26708
27043
  HlsVideoPlayer.displayName = "HlsVideoPlayer";
26709
27044
  var VideoPlayer = HlsVideoPlayer;
@@ -26711,6 +27046,7 @@ var CroppedHlsVideoPlayer = React23.forwardRef(({
26711
27046
  crop,
26712
27047
  debug = false,
26713
27048
  onClick,
27049
+ controls = true,
26714
27050
  ...videoProps
26715
27051
  }, ref) => {
26716
27052
  const {
@@ -26732,9 +27068,15 @@ var CroppedHlsVideoPlayer = React23.forwardRef(({
26732
27068
  const [isVideoReady, setIsVideoReady] = React23.useState(false);
26733
27069
  const [canvasDimensions, setCanvasDimensions] = React23.useState({ width: 0, height: 0 });
26734
27070
  const [isProcessing, setIsProcessing] = React23.useState(false);
26735
- const [showIndicator, setShowIndicator] = React23.useState(false);
26736
- const [indicatorIsPlaying, setIndicatorIsPlaying] = React23.useState(false);
26737
- const indicatorKeyRef = React23.useRef(0);
27071
+ const [showControls, setShowControls] = React23.useState(true);
27072
+ const [isPlaying, setIsPlaying] = React23.useState(false);
27073
+ const [currentTime, setCurrentTime] = React23.useState(0);
27074
+ const [duration, setDuration] = React23.useState(0);
27075
+ const [buffered, setBuffered] = React23.useState(0);
27076
+ const [playbackRate, setPlaybackRate] = React23.useState(1);
27077
+ const controlsTimeoutRef = React23.useRef(null);
27078
+ const userSeekingRef = React23.useRef(false);
27079
+ const [controlsPinned, setControlsPinned] = React23.useState(false);
26738
27080
  const stopCanvasRendering = React23.useCallback(() => {
26739
27081
  if (animationFrameRef.current) {
26740
27082
  cancelAnimationFrame(animationFrameRef.current);
@@ -26819,7 +27161,7 @@ var CroppedHlsVideoPlayer = React23.forwardRef(({
26819
27161
  const canvas = canvasRef.current;
26820
27162
  const video = videoElementRef.current;
26821
27163
  const ctx = canvas.getContext("2d");
26822
- if (!ctx || video.paused || video.ended) {
27164
+ if (!ctx || video.readyState < 2) {
26823
27165
  return;
26824
27166
  }
26825
27167
  const videoWidth = video.videoWidth;
@@ -26846,7 +27188,9 @@ var CroppedHlsVideoPlayer = React23.forwardRef(({
26846
27188
  canvas.height
26847
27189
  // Destination (full canvas)
26848
27190
  );
26849
- animationFrameRef.current = requestAnimationFrame(renderFrameToCanvas);
27191
+ if (!video.paused && !video.ended) {
27192
+ animationFrameRef.current = requestAnimationFrame(renderFrameToCanvas);
27193
+ }
26850
27194
  }, [crop]);
26851
27195
  const handleVideoReady = React23.useCallback((player) => {
26852
27196
  console.log("[CroppedHlsVideoPlayer] Video player ready");
@@ -26854,11 +27198,15 @@ var CroppedHlsVideoPlayer = React23.forwardRef(({
26854
27198
  if (videoEl) {
26855
27199
  videoElementRef.current = videoEl;
26856
27200
  setIsVideoReady(true);
27201
+ if (videoEl.readyState >= 2) {
27202
+ renderFrameToCanvas();
27203
+ }
26857
27204
  }
26858
27205
  onReadyProp?.(player);
26859
- }, [onReadyProp]);
27206
+ }, [onReadyProp, renderFrameToCanvas]);
26860
27207
  const handleVideoPlay = React23.useCallback((player) => {
26861
27208
  console.log("[CroppedHlsVideoPlayer] Video playing, starting canvas rendering");
27209
+ setIsPlaying(true);
26862
27210
  if (crop && canvasRef.current) {
26863
27211
  setIsProcessing(true);
26864
27212
  renderFrameToCanvas();
@@ -26866,43 +27214,40 @@ var CroppedHlsVideoPlayer = React23.forwardRef(({
26866
27214
  onPlayProp?.(player);
26867
27215
  }, [crop, renderFrameToCanvas, onPlayProp]);
26868
27216
  const handleVideoPause = React23.useCallback((player) => {
26869
- console.log("[CroppedHlsVideoPlayer] Video paused, stopping canvas rendering and CLEARING canvas");
27217
+ console.log("[CroppedHlsVideoPlayer] Video paused, stopping canvas rendering (keeping last frame)");
27218
+ if (userSeekingRef.current && hiddenVideoRef.current) {
27219
+ hiddenVideoRef.current.play()?.catch(() => {
27220
+ });
27221
+ return;
27222
+ }
26870
27223
  stopCanvasRendering();
26871
27224
  setIsProcessing(false);
26872
- if (canvasRef.current) {
26873
- const ctx = canvasRef.current.getContext("2d");
26874
- if (ctx) {
26875
- ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
26876
- ctx.fillStyle = "black";
26877
- ctx.fillRect(0, 0, canvasRef.current.width, canvasRef.current.height);
26878
- }
26879
- }
27225
+ setIsPlaying(false);
27226
+ renderFrameToCanvas();
26880
27227
  onPauseProp?.(player);
26881
- }, [stopCanvasRendering, onPauseProp]);
27228
+ }, [stopCanvasRendering, onPauseProp, renderFrameToCanvas]);
26882
27229
  const handleVideoEnded = React23.useCallback((player) => {
26883
- console.log("[CroppedHlsVideoPlayer] Video ended, CLEARING canvas");
27230
+ console.log("[CroppedHlsVideoPlayer] Video ended, stopping canvas rendering (keeping last frame)");
26884
27231
  stopCanvasRendering();
26885
27232
  setIsProcessing(false);
26886
- if (canvasRef.current) {
26887
- const ctx = canvasRef.current.getContext("2d");
26888
- if (ctx) {
26889
- ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
26890
- ctx.fillStyle = "black";
26891
- ctx.fillRect(0, 0, canvasRef.current.width, canvasRef.current.height);
26892
- }
26893
- }
27233
+ setIsPlaying(false);
27234
+ userSeekingRef.current = false;
26894
27235
  onEndedProp?.(player);
26895
27236
  }, [stopCanvasRendering, onEndedProp]);
26896
27237
  const handleSeeking = React23.useCallback((player) => {
26897
27238
  console.log("[CroppedHlsVideoPlayer] Video seeking");
26898
- if (crop && !videoElementRef.current?.paused) {
27239
+ userSeekingRef.current = true;
27240
+ if (crop) {
26899
27241
  renderFrameToCanvas();
26900
27242
  }
26901
27243
  onSeekingProp?.(player);
26902
27244
  }, [crop, renderFrameToCanvas, onSeekingProp]);
26903
27245
  const handleSeeked = React23.useCallback((player) => {
26904
27246
  console.log("[CroppedHlsVideoPlayer] Video seeked");
26905
- if (crop && !videoElementRef.current?.paused) {
27247
+ hiddenVideoRef.current?.play()?.catch(() => {
27248
+ });
27249
+ userSeekingRef.current = false;
27250
+ if (crop) {
26906
27251
  renderFrameToCanvas();
26907
27252
  }
26908
27253
  onSeekedProp?.(player);
@@ -26910,8 +27255,29 @@ var CroppedHlsVideoPlayer = React23.forwardRef(({
26910
27255
  const handleLoadedMetadata = React23.useCallback((player) => {
26911
27256
  console.log("[CroppedHlsVideoPlayer] Video metadata loaded");
26912
27257
  calculateCanvasDimensions();
27258
+ if (hiddenVideoRef.current?.video) {
27259
+ setDuration(hiddenVideoRef.current.video.duration || 0);
27260
+ }
27261
+ requestAnimationFrame(() => renderFrameToCanvas());
26913
27262
  onLoadedMetadataProp?.(player);
26914
- }, [calculateCanvasDimensions, onLoadedMetadataProp]);
27263
+ }, [calculateCanvasDimensions, onLoadedMetadataProp, renderFrameToCanvas]);
27264
+ const handleTimeUpdate = React23.useCallback((player, time2) => {
27265
+ setCurrentTime(time2);
27266
+ if (hiddenVideoRef.current?.video && hiddenVideoRef.current.video.buffered.length > 0) {
27267
+ const video = hiddenVideoRef.current.video;
27268
+ for (let i = 0; i < video.buffered.length; i++) {
27269
+ if (video.buffered.start(i) <= time2 && video.buffered.end(i) >= time2) {
27270
+ setBuffered(video.buffered.end(i));
27271
+ break;
27272
+ }
27273
+ }
27274
+ }
27275
+ videoProps.onTimeUpdate?.(player, time2);
27276
+ }, [videoProps.onTimeUpdate]);
27277
+ const handleDurationChange = React23.useCallback((player, dur) => {
27278
+ setDuration(dur);
27279
+ videoProps.onDurationChange?.(player, dur);
27280
+ }, [videoProps.onDurationChange]);
26915
27281
  React23.useEffect(() => {
26916
27282
  calculateCanvasDimensions();
26917
27283
  const handleResize = () => {
@@ -26939,33 +27305,97 @@ var CroppedHlsVideoPlayer = React23.forwardRef(({
26939
27305
  stopCanvasRendering();
26940
27306
  };
26941
27307
  }, [stopCanvasRendering]);
27308
+ const resetControlsTimeout = React23.useCallback(() => {
27309
+ if (controlsPinned) {
27310
+ setShowControls(true);
27311
+ return;
27312
+ }
27313
+ setShowControls(true);
27314
+ if (controlsTimeoutRef.current) {
27315
+ clearTimeout(controlsTimeoutRef.current);
27316
+ }
27317
+ if (isPlaying) {
27318
+ controlsTimeoutRef.current = setTimeout(() => {
27319
+ setShowControls(false);
27320
+ }, 3e3);
27321
+ }
27322
+ }, [isPlaying, controlsPinned]);
27323
+ const handleMouseMove = React23.useCallback(() => {
27324
+ resetControlsTimeout();
27325
+ }, [resetControlsTimeout]);
27326
+ React23.useEffect(() => {
27327
+ resetControlsTimeout();
27328
+ return () => {
27329
+ if (controlsTimeoutRef.current) {
27330
+ clearTimeout(controlsTimeoutRef.current);
27331
+ }
27332
+ };
27333
+ }, [isPlaying, resetControlsTimeout]);
27334
+ const handleTogglePlay = React23.useCallback(() => {
27335
+ if (hiddenVideoRef.current?.video) {
27336
+ if (hiddenVideoRef.current.video.paused) {
27337
+ hiddenVideoRef.current.play();
27338
+ } else {
27339
+ hiddenVideoRef.current.pause();
27340
+ }
27341
+ }
27342
+ }, []);
27343
+ const handleSeek = React23.useCallback((time2) => {
27344
+ if (hiddenVideoRef.current) {
27345
+ hiddenVideoRef.current.currentTime(time2);
27346
+ hiddenVideoRef.current.play()?.catch(() => {
27347
+ });
27348
+ setTimeout(() => renderFrameToCanvas(), 50);
27349
+ }
27350
+ }, [renderFrameToCanvas]);
27351
+ const handleSeekStart = React23.useCallback(() => {
27352
+ userSeekingRef.current = true;
27353
+ }, []);
27354
+ const handleSeekEnd = React23.useCallback(() => {
27355
+ if (hiddenVideoRef.current) {
27356
+ hiddenVideoRef.current.play()?.catch(() => {
27357
+ });
27358
+ }
27359
+ }, []);
27360
+ const handlePlaybackRateChange = React23.useCallback((rate) => {
27361
+ if (hiddenVideoRef.current) {
27362
+ hiddenVideoRef.current.playbackRate(rate);
27363
+ setPlaybackRate(rate);
27364
+ }
27365
+ }, []);
27366
+ const handleToggleFullscreen = React23.useCallback(() => {
27367
+ if (videoContainerRef.current) {
27368
+ if (!document.fullscreenElement) {
27369
+ videoContainerRef.current.requestFullscreen();
27370
+ } else {
27371
+ document.exitFullscreen();
27372
+ }
27373
+ }
27374
+ }, []);
26942
27375
  if (!crop) {
26943
- return /* @__PURE__ */ jsxRuntime.jsx(HlsVideoPlayer, { ref, ...videoProps, onClick });
26944
- }
26945
- const handleClickWithIndicator = () => {
26946
- if (!onClick || !hiddenVideoRef.current?.video) return;
26947
- const video = hiddenVideoRef.current.video;
26948
- const willBePlaying = video.paused;
26949
- setIndicatorIsPlaying(willBePlaying);
26950
- setShowIndicator(false);
26951
- setTimeout(() => {
26952
- indicatorKeyRef.current += 1;
26953
- setShowIndicator(true);
26954
- }, 0);
26955
- onClick();
27376
+ return /* @__PURE__ */ jsxRuntime.jsx(HlsVideoPlayer, { ref, ...videoProps, onClick, controls });
27377
+ }
27378
+ const handleClick = () => {
27379
+ if (!onClick && !controls) {
27380
+ handleTogglePlay();
27381
+ }
27382
+ if (onClick) onClick();
26956
27383
  };
26957
27384
  return /* @__PURE__ */ jsxRuntime.jsxs(
26958
27385
  "div",
26959
27386
  {
26960
27387
  ref: videoContainerRef,
26961
- className: `relative w-full h-full flex items-center justify-center bg-black ${onClick ? "cursor-pointer" : ""} ${inheritedClassName}`,
26962
- onClick: handleClickWithIndicator,
27388
+ className: `relative w-full h-full flex items-center justify-center bg-black group ${inheritedClassName} ${onClick || controls ? "cursor-pointer" : ""}`,
27389
+ onClick: handleClick,
27390
+ onMouseMove: handleMouseMove,
27391
+ onMouseLeave: () => isPlaying && !controlsPinned && setShowControls(false),
26963
27392
  children: [
26964
27393
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
26965
27394
  HlsVideoPlayer,
26966
27395
  {
26967
27396
  ref: hiddenVideoRef,
26968
27397
  ...videoProps,
27398
+ controls: false,
26969
27399
  onReady: handleVideoReady,
26970
27400
  onPlay: handleVideoPlay,
26971
27401
  onPause: handleVideoPause,
@@ -26975,7 +27405,9 @@ var CroppedHlsVideoPlayer = React23.forwardRef(({
26975
27405
  onLoadedMetadata: handleLoadedMetadata,
26976
27406
  onLoadedData: videoProps.onLoadedData,
26977
27407
  onPlaying: videoProps.onPlaying,
26978
- onLoadingChange: videoProps.onLoadingChange
27408
+ onLoadingChange: videoProps.onLoadingChange,
27409
+ onTimeUpdate: handleTimeUpdate,
27410
+ onDurationChange: handleDurationChange
26979
27411
  }
26980
27412
  ) }),
26981
27413
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -26992,8 +27424,8 @@ var CroppedHlsVideoPlayer = React23.forwardRef(({
26992
27424
  }
26993
27425
  }
26994
27426
  ),
26995
- !isVideoReady && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(OptifyeLogoLoader_default, { size: "md", message: "Loading video..." }) }),
26996
- debug && isVideoReady && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute top-2 left-2 bg-black/80 text-white text-xs p-2 rounded font-mono", children: [
27427
+ !isVideoReady && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 flex items-center justify-center pointer-events-none", children: /* @__PURE__ */ jsxRuntime.jsx(OptifyeLogoLoader_default, { size: "md", message: "Loading video..." }) }),
27428
+ debug && isVideoReady && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute top-2 left-2 bg-black/80 text-white text-xs p-2 rounded font-mono pointer-events-none z-20", children: [
26997
27429
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
26998
27430
  "Crop: ",
26999
27431
  crop.x,
@@ -27017,13 +27449,32 @@ var CroppedHlsVideoPlayer = React23.forwardRef(({
27017
27449
  isProcessing ? "Yes" : "No"
27018
27450
  ] })
27019
27451
  ] }),
27020
- onClick && /* @__PURE__ */ jsxRuntime.jsx(
27021
- PlayPauseIndicator,
27452
+ controls && isVideoReady && /* @__PURE__ */ jsxRuntime.jsx(
27453
+ VideoControls,
27022
27454
  {
27023
- show: showIndicator,
27024
- isPlaying: indicatorIsPlaying
27025
- },
27026
- indicatorKeyRef.current
27455
+ isPlaying,
27456
+ currentTime,
27457
+ duration,
27458
+ buffered,
27459
+ showControls: controlsPinned || showControls || !isPlaying,
27460
+ controlsPinned,
27461
+ playbackRate,
27462
+ onPlayPause: handleTogglePlay,
27463
+ onSeek: handleSeek,
27464
+ onSeekStart: handleSeekStart,
27465
+ onSeekEnd: handleSeekEnd,
27466
+ onPlaybackRateChange: handlePlaybackRateChange,
27467
+ onTogglePinControls: () => setControlsPinned((prev) => {
27468
+ const next = !prev;
27469
+ if (next) {
27470
+ setShowControls(true);
27471
+ } else {
27472
+ resetControlsTimeout();
27473
+ }
27474
+ return next;
27475
+ }),
27476
+ onToggleFullscreen: handleToggleFullscreen
27477
+ }
27027
27478
  )
27028
27479
  ]
27029
27480
  }
@@ -27562,6 +28013,67 @@ var SilentErrorBoundary = class extends React23__namespace.default.Component {
27562
28013
  ] }) });
27563
28014
  }
27564
28015
  };
28016
+ var PlayPauseIndicator = ({
28017
+ show,
28018
+ isPlaying,
28019
+ duration = 600
28020
+ }) => {
28021
+ const [isVisible, setIsVisible] = React23.useState(false);
28022
+ const [isFading, setIsFading] = React23.useState(false);
28023
+ React23.useEffect(() => {
28024
+ if (show) {
28025
+ setIsVisible(true);
28026
+ setIsFading(false);
28027
+ const fadeTimer = setTimeout(() => {
28028
+ setIsFading(true);
28029
+ }, 100);
28030
+ const hideTimer = setTimeout(() => {
28031
+ setIsVisible(false);
28032
+ setIsFading(false);
28033
+ }, duration);
28034
+ return () => {
28035
+ clearTimeout(fadeTimer);
28036
+ clearTimeout(hideTimer);
28037
+ };
28038
+ }
28039
+ }, [show, duration]);
28040
+ if (!isVisible) return null;
28041
+ return /* @__PURE__ */ jsxRuntime.jsx(
28042
+ "div",
28043
+ {
28044
+ className: "absolute inset-0 flex items-center justify-center pointer-events-none z-10",
28045
+ style: {
28046
+ opacity: isFading ? 0 : 1,
28047
+ transition: `opacity ${duration - 100}ms ease-out`
28048
+ },
28049
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-black/70 rounded-full p-6", children: isPlaying ? (
28050
+ // Play icon (triangle)
28051
+ /* @__PURE__ */ jsxRuntime.jsx(
28052
+ "svg",
28053
+ {
28054
+ xmlns: "http://www.w3.org/2000/svg",
28055
+ viewBox: "0 0 24 24",
28056
+ fill: "white",
28057
+ className: "w-16 h-16",
28058
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 5v14l11-7z" })
28059
+ }
28060
+ )
28061
+ ) : (
28062
+ // Pause icon (two bars)
28063
+ /* @__PURE__ */ jsxRuntime.jsx(
28064
+ "svg",
28065
+ {
28066
+ xmlns: "http://www.w3.org/2000/svg",
28067
+ viewBox: "0 0 24 24",
28068
+ fill: "white",
28069
+ className: "w-16 h-16",
28070
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 4h4v16H6V4zm8 0h4v16h-4V4z" })
28071
+ }
28072
+ )
28073
+ ) })
28074
+ }
28075
+ );
28076
+ };
27565
28077
  var BackButton = ({
27566
28078
  onClick,
27567
28079
  text = "Back",
@@ -29214,6 +29726,31 @@ var BottlenecksContent = ({
29214
29726
  const [categoryMetadata, setCategoryMetadata] = React23.useState([]);
29215
29727
  const [currentMetadataIndex, setCurrentMetadataIndex] = React23.useState(0);
29216
29728
  const [metadataCache, setMetadataCache] = React23.useState({});
29729
+ const invalidateMetadataCache = React23.useCallback((categories) => {
29730
+ setMetadataCache((prevCache) => {
29731
+ if (!prevCache || Object.keys(prevCache).length === 0) {
29732
+ return prevCache;
29733
+ }
29734
+ const targetCategories = categories ? (Array.isArray(categories) ? categories : [categories]).filter(Boolean) : null;
29735
+ let updatedCache = null;
29736
+ const shouldInvalidate = (key) => {
29737
+ if (!targetCategories || targetCategories.length === 0) {
29738
+ return true;
29739
+ }
29740
+ const [categoryId] = key.split("-");
29741
+ return targetCategories.includes(categoryId);
29742
+ };
29743
+ Object.keys(prevCache).forEach((cacheKey) => {
29744
+ if (shouldInvalidate(cacheKey)) {
29745
+ if (!updatedCache) {
29746
+ updatedCache = { ...prevCache };
29747
+ }
29748
+ delete updatedCache[cacheKey];
29749
+ }
29750
+ });
29751
+ return updatedCache || prevCache;
29752
+ });
29753
+ }, []);
29217
29754
  const [triageClips, setTriageClips] = React23.useState([]);
29218
29755
  const [isLoadingTriageClips, setIsLoadingTriageClips] = React23.useState(false);
29219
29756
  const [isFullscreen, setIsFullscreen] = React23.useState(false);
@@ -29233,6 +29770,12 @@ var BottlenecksContent = ({
29233
29770
  onNewClips: (notification) => {
29234
29771
  console.log(`[BottlenecksContent] New clips detected:`, notification);
29235
29772
  if (notification.clips.length > 0) {
29773
+ const categoryIds = notification.clips.map((clip) => clip.clip_type).filter(Boolean).map((value) => String(value));
29774
+ if (categoryIds.length > 0) {
29775
+ invalidateMetadataCache(categoryIds);
29776
+ } else {
29777
+ invalidateMetadataCache();
29778
+ }
29236
29779
  fetchClipCounts();
29237
29780
  }
29238
29781
  }
@@ -29279,24 +29822,37 @@ var BottlenecksContent = ({
29279
29822
  shift: shift || "0"
29280
29823
  });
29281
29824
  React23.useEffect(() => {
29282
- if (clipTypes.length > 0 && !initialFilter) {
29283
- const priorityOrder = ["cycle_completion", "fast-cycles", "slow-cycles", "idle_time"];
29284
- let selectedType = null;
29285
- for (const priorityType of priorityOrder) {
29286
- const type = clipTypes.find((t) => t.type === priorityType && (dynamicCounts[t.type] || 0) > 0);
29287
- if (type) {
29288
- selectedType = type;
29289
- break;
29825
+ if (clipTypes.length > 0) {
29826
+ const currentFilterCount = initialFilter ? dynamicCounts[initialFilter] || 0 : 0;
29827
+ const hasAnyCounts = Object.values(dynamicCounts).some((c) => c > 0);
29828
+ const userHasNotNavigated = !initialFilter || activeFilterRef.current === initialFilter;
29829
+ const shouldRunSelection = !initialFilter || userHasNotNavigated && currentFilterCount === 0 && hasAnyCounts;
29830
+ if (shouldRunSelection) {
29831
+ let selectedType = null;
29832
+ if (clipTypes.length === 1) {
29833
+ selectedType = clipTypes[0];
29834
+ } else {
29835
+ const priorityOrder = ["cycle_completion", "fast-cycles", "slow-cycles", "idle_time"];
29836
+ for (const priorityType of priorityOrder) {
29837
+ const type = clipTypes.find((t) => t.type === priorityType && (dynamicCounts[t.type] || 0) > 0);
29838
+ if (type) {
29839
+ selectedType = type;
29840
+ break;
29841
+ }
29842
+ }
29843
+ if (!selectedType) {
29844
+ selectedType = clipTypes.find((type) => (dynamicCounts[type.type] || 0) > 0);
29845
+ }
29846
+ if (!selectedType) {
29847
+ selectedType = clipTypes[0];
29848
+ }
29849
+ }
29850
+ if (selectedType && selectedType.type !== initialFilter) {
29851
+ console.log(`[BottlenecksContent] Auto-selecting filter: ${selectedType.type} (count: ${dynamicCounts[selectedType.type] || 0})`);
29852
+ setInitialFilter(selectedType.type);
29853
+ setActiveFilter(selectedType.type);
29854
+ activeFilterRef.current = selectedType.type;
29290
29855
  }
29291
- }
29292
- if (!selectedType) {
29293
- selectedType = clipTypes.find((type) => (dynamicCounts[type.type] || 0) > 0);
29294
- }
29295
- const firstType = selectedType || clipTypes[0];
29296
- if (firstType) {
29297
- setInitialFilter(firstType.type);
29298
- setActiveFilter(firstType.type);
29299
- activeFilterRef.current = firstType.type;
29300
29856
  }
29301
29857
  }
29302
29858
  }, [clipTypes, dynamicCounts, initialFilter]);
@@ -29348,7 +29904,7 @@ var BottlenecksContent = ({
29348
29904
  } finally {
29349
29905
  fetchInProgressRef.current.delete(operationKey);
29350
29906
  }
29351
- }, [workspaceId, date, s3ClipsService, effectiveShift, dashboardConfig, updateClipCounts]);
29907
+ }, [workspaceId, date, s3ClipsService, effectiveShift, dashboardConfig, updateClipCounts, timezone, totalOutput]);
29352
29908
  const loadingCategoryRef = React23.useRef(null);
29353
29909
  const loadFirstVideoForCategory = React23.useCallback(async (category) => {
29354
29910
  if (!workspaceId || !s3ClipsService || !isMountedRef.current) return;
@@ -29438,15 +29994,16 @@ var BottlenecksContent = ({
29438
29994
  loadingCategoryRef.current = null;
29439
29995
  fetchInProgressRef.current.delete(operationKey);
29440
29996
  }
29441
- }, [workspaceId, date, s3ClipsService, mergedCounts, effectiveShift]);
29997
+ }, [workspaceId, date, s3ClipsService, mergedCounts, effectiveShift, timezone]);
29442
29998
  const handleRefreshClips = React23.useCallback(async () => {
29443
29999
  console.log("[BottlenecksContent] Refreshing clips after new additions");
29444
30000
  acknowledgeNewClips();
30001
+ invalidateMetadataCache();
29445
30002
  await fetchClipCounts();
29446
30003
  if (activeFilter && mergedCounts[activeFilter] > 0) {
29447
30004
  await loadFirstVideoForCategory(activeFilter);
29448
30005
  }
29449
- }, [acknowledgeNewClips, fetchClipCounts, activeFilter, mergedCounts, loadFirstVideoForCategory]);
30006
+ }, [acknowledgeNewClips, fetchClipCounts, activeFilter, mergedCounts, loadFirstVideoForCategory, invalidateMetadataCache]);
29450
30007
  React23.useEffect(() => {
29451
30008
  if (s3ClipsService) {
29452
30009
  fetchClipCounts();
@@ -29616,38 +30173,38 @@ var BottlenecksContent = ({
29616
30173
  loadingTimeoutRef.current = null;
29617
30174
  }
29618
30175
  }, []);
29619
- const loadCategoryMetadata = React23.useCallback(async (categoryId, autoLoadFirstVideo = false) => {
30176
+ const loadCategoryMetadata = React23.useCallback(async (categoryId, autoLoadFirstVideo = false, forceRefresh = false) => {
29620
30177
  if (!workspaceId) {
29621
30178
  return;
29622
30179
  }
29623
- const cacheKey = `${categoryId}-${date || getOperationalDate(timezone)}-${effectiveShift}`;
29624
- if (metadataCache[cacheKey]) {
29625
- console.log(`[BottlenecksContent] Using cached metadata for ${categoryId}`);
29626
- setCategoryMetadata(metadataCache[cacheKey]);
29627
- categoryMetadataRef.current = metadataCache[cacheKey];
29628
- if (autoLoadFirstVideo && metadataCache[cacheKey].length > 0 && s3ClipsService) {
29629
- const firstClipMeta = metadataCache[cacheKey][0];
29630
- try {
29631
- const video = await s3ClipsService.getClipById(firstClipMeta.clipId);
29632
- if (video && isMountedRef.current) {
29633
- setCurrentClipId(firstClipMeta.clipId);
29634
- setAllVideos([video]);
29635
- setCurrentIndex(0);
29636
- setCurrentMetadataIndex(0);
29637
- currentMetadataIndexRef.current = 0;
29638
- setIsCategoryLoading(false);
29639
- console.log(`[BottlenecksContent] Auto-loaded first video from cache: ${video.id} (1/${metadataCache[cacheKey].length})`);
30180
+ const resolvedDate = date || getOperationalDate(timezone);
30181
+ const cacheKey = `${categoryId}-${resolvedDate}-${effectiveShift}`;
30182
+ const cachedMetadata = !forceRefresh ? metadataCache[cacheKey] : void 0;
30183
+ try {
30184
+ if (cachedMetadata) {
30185
+ console.log(`[BottlenecksContent] Using cached metadata for ${categoryId}`);
30186
+ setCategoryMetadata(cachedMetadata);
30187
+ categoryMetadataRef.current = cachedMetadata;
30188
+ if (autoLoadFirstVideo && cachedMetadata.length > 0 && s3ClipsService) {
30189
+ const firstClipMeta = cachedMetadata[0];
30190
+ try {
30191
+ const video = await s3ClipsService.getClipById(firstClipMeta.clipId);
30192
+ if (video && isMountedRef.current) {
30193
+ setCurrentClipId(firstClipMeta.clipId);
30194
+ setAllVideos([video]);
30195
+ setCurrentIndex(0);
30196
+ setCurrentMetadataIndex(0);
30197
+ currentMetadataIndexRef.current = 0;
30198
+ console.log(`[BottlenecksContent] Auto-loaded first video from cache: ${video.id} (1/${cachedMetadata.length})`);
30199
+ }
30200
+ } catch (error2) {
30201
+ console.error(`[BottlenecksContent] Error loading first video from cache:`, error2);
30202
+ clearLoadingState();
29640
30203
  }
29641
- } catch (error2) {
29642
- console.error(`[BottlenecksContent] Error loading first video from cache:`, error2);
29643
- setIsCategoryLoading(false);
29644
- clearLoadingState();
29645
30204
  }
30205
+ return;
29646
30206
  }
29647
- return;
29648
- }
29649
- try {
29650
- console.log(`[BottlenecksContent] Loading metadata for category: ${categoryId}`);
30207
+ console.log(`[BottlenecksContent] Loading metadata for category: ${categoryId}${forceRefresh ? " (force refresh)" : ""}`);
29651
30208
  const { createClient: createClient5 } = await import('@supabase/supabase-js');
29652
30209
  const supabase = createClient5(
29653
30210
  process.env.NEXT_PUBLIC_SUPABASE_URL || "",
@@ -29672,8 +30229,8 @@ var BottlenecksContent = ({
29672
30229
  action: "percentile-clips",
29673
30230
  percentileAction: percentileType,
29674
30231
  workspaceId,
29675
- startDate: `${date || getOperationalDate(timezone)}T00:00:00Z`,
29676
- endDate: `${date || getOperationalDate(timezone)}T23:59:59Z`,
30232
+ startDate: `${resolvedDate}T00:00:00Z`,
30233
+ endDate: `${resolvedDate}T23:59:59Z`,
29677
30234
  percentile: 10,
29678
30235
  shiftId: effectiveShift,
29679
30236
  limit: 100
@@ -29689,7 +30246,7 @@ var BottlenecksContent = ({
29689
30246
  body: JSON.stringify({
29690
30247
  action: "clip-metadata",
29691
30248
  workspaceId,
29692
- date: date || getOperationalDate(timezone),
30249
+ date: resolvedDate,
29693
30250
  shift: effectiveShift,
29694
30251
  category: categoryId,
29695
30252
  page: 1,
@@ -29734,19 +30291,22 @@ var BottlenecksContent = ({
29734
30291
  setCurrentIndex(0);
29735
30292
  setCurrentMetadataIndex(0);
29736
30293
  currentMetadataIndexRef.current = 0;
29737
- setIsCategoryLoading(false);
29738
30294
  console.log(`[BottlenecksContent] Auto-loaded first video: ${video.id} (1/${metadataClips.length})`);
29739
30295
  }
29740
30296
  } catch (error2) {
29741
30297
  console.error(`[BottlenecksContent] Error loading first video:`, error2);
29742
- setIsCategoryLoading(false);
29743
30298
  }
29744
30299
  }
30300
+ } else {
30301
+ setCategoryMetadata([]);
30302
+ categoryMetadataRef.current = [];
29745
30303
  }
29746
30304
  } catch (error2) {
29747
30305
  console.error(`[BottlenecksContent] Error loading category metadata:`, error2);
30306
+ } finally {
30307
+ setIsCategoryLoading(false);
29748
30308
  }
29749
- }, [workspaceId, date, effectiveShift, isPercentileCategory, metadataCache, s3ClipsService]);
30309
+ }, [workspaceId, date, effectiveShift, isPercentileCategory, metadataCache, s3ClipsService, timezone, clearLoadingState]);
29750
30310
  const loadAndPlayClipById = React23.useCallback(async (clipId, categoryId, position) => {
29751
30311
  if (!workspaceId || !s3ClipsService || !isMountedRef.current) return;
29752
30312
  console.log(`[BottlenecksContent] Loading clip by ID: ${clipId}, category=${categoryId}, position=${position}`);
@@ -29770,21 +30330,31 @@ var BottlenecksContent = ({
29770
30330
  }
29771
30331
  try {
29772
30332
  await loadCategoryMetadata(categoryId, false);
29773
- const metadataArray = categoryMetadataRef.current;
29774
- if (metadataArray.length > 0) {
29775
- const clickedClipIndex = metadataArray.findIndex((clip) => clip.clipId === clipId);
29776
- if (clickedClipIndex !== -1) {
29777
- setCurrentMetadataIndex(clickedClipIndex);
29778
- currentMetadataIndexRef.current = clickedClipIndex;
29779
- const video = await s3ClipsService.getClipById(clipId);
29780
- if (video) {
29781
- setPendingVideo(video);
29782
- setCurrentClipId(clipId);
29783
- setAllVideos([video]);
29784
- setCurrentIndex(0);
29785
- console.log(`[BottlenecksContent] Loaded clip ${clipId} (${clickedClipIndex + 1}/${metadataArray.length})`);
29786
- }
29787
- }
30333
+ let metadataArray = categoryMetadataRef.current;
30334
+ const clipExistsInMetadata = metadataArray.some((clip) => clip.clipId === clipId);
30335
+ if (metadataArray.length === 0 || !clipExistsInMetadata) {
30336
+ console.warn(`[BottlenecksContent] Clip ${clipId} not found in metadata for ${categoryId} (cache hit: ${metadataArray.length > 0}) - forcing refresh`);
30337
+ await loadCategoryMetadata(categoryId, false, true);
30338
+ metadataArray = categoryMetadataRef.current;
30339
+ }
30340
+ if (metadataArray.length === 0) {
30341
+ throw new Error(`No metadata available for category ${categoryId}`);
30342
+ }
30343
+ const clickedClipIndex = metadataArray.findIndex((clip) => clip.clipId === clipId);
30344
+ if (clickedClipIndex === -1) {
30345
+ throw new Error(`Clip ${clipId} not found after metadata refresh`);
30346
+ }
30347
+ setCurrentMetadataIndex(clickedClipIndex);
30348
+ currentMetadataIndexRef.current = clickedClipIndex;
30349
+ const video = await s3ClipsService.getClipById(clipId);
30350
+ if (video) {
30351
+ setPendingVideo(video);
30352
+ setCurrentClipId(clipId);
30353
+ setAllVideos([video]);
30354
+ setCurrentIndex(0);
30355
+ console.log(`[BottlenecksContent] Loaded clip ${clipId} (${clickedClipIndex + 1}/${metadataArray.length})`);
30356
+ } else {
30357
+ throw new Error(`Failed to load video data for clip ${clipId}`);
29788
30358
  }
29789
30359
  } catch (error2) {
29790
30360
  console.error(`[BottlenecksContent] Error loading clip by ID (${clipId}):`, error2);
@@ -29798,7 +30368,7 @@ var BottlenecksContent = ({
29798
30368
  clearLoadingState();
29799
30369
  }
29800
30370
  }
29801
- }, [workspaceId, s3ClipsService, date, effectiveShift, updateActiveFilter, clearLoadingState]);
30371
+ }, [workspaceId, s3ClipsService, updateActiveFilter, clearLoadingState, loadCategoryMetadata]);
29802
30372
  React23.useCallback(async (categoryId, clipIndex) => {
29803
30373
  console.warn("[BottlenecksContent] loadAndPlayClip is deprecated, use loadAndPlayClipById instead");
29804
30374
  if (!workspaceId || !s3ClipsService || !isMountedRef.current) return;
@@ -29825,7 +30395,7 @@ var BottlenecksContent = ({
29825
30395
  });
29826
30396
  setIsNavigating(false);
29827
30397
  }
29828
- }, [workspaceId, s3ClipsService, date, effectiveShift, loadAndPlayClipById]);
30398
+ }, [workspaceId, s3ClipsService, date, effectiveShift, loadAndPlayClipById, timezone]);
29829
30399
  const handleNext = React23.useCallback(async () => {
29830
30400
  if (!isMountedRef.current) return;
29831
30401
  const currentFilter = activeFilterRef.current;
@@ -29842,8 +30412,18 @@ var BottlenecksContent = ({
29842
30412
  }
29843
30413
  try {
29844
30414
  const currentMetaIndex = currentMetadataIndexRef.current;
29845
- const metadataArray = categoryMetadataRef.current;
30415
+ let metadataArray = categoryMetadataRef.current;
30416
+ if (metadataArray.length === 0) {
30417
+ console.log(`[handleNext] Metadata empty for ${currentFilter}, loading before navigation`);
30418
+ await loadCategoryMetadata(currentFilter, false);
30419
+ metadataArray = categoryMetadataRef.current;
30420
+ }
29846
30421
  console.log(`[handleNext] Unified navigation: ${currentFilter}, metadata index: ${currentMetaIndex}/${metadataArray.length}`);
30422
+ if (metadataArray.length === 0) {
30423
+ console.warn("[handleNext] No metadata available after refresh - stopping navigation");
30424
+ clearLoadingState();
30425
+ return;
30426
+ }
29847
30427
  if (currentMetaIndex < metadataArray.length - 1) {
29848
30428
  const nextMetadataIndex = currentMetaIndex + 1;
29849
30429
  const nextClipMeta = metadataArray[nextMetadataIndex];
@@ -29878,7 +30458,7 @@ var BottlenecksContent = ({
29878
30458
  });
29879
30459
  clearLoadingState();
29880
30460
  }
29881
- }, [clearLoadingState, s3ClipsService]);
30461
+ }, [clearLoadingState, s3ClipsService, loadCategoryMetadata]);
29882
30462
  const handlePrevious = React23.useCallback(async () => {
29883
30463
  if (!isMountedRef.current) return;
29884
30464
  const currentFilter = activeFilterRef.current;
@@ -29895,8 +30475,18 @@ var BottlenecksContent = ({
29895
30475
  }
29896
30476
  try {
29897
30477
  const currentMetaIndex = currentMetadataIndexRef.current;
29898
- const metadataArray = categoryMetadataRef.current;
30478
+ let metadataArray = categoryMetadataRef.current;
30479
+ if (metadataArray.length === 0) {
30480
+ console.log(`[handlePrevious] Metadata empty for ${currentFilter}, loading before navigation`);
30481
+ await loadCategoryMetadata(currentFilter, false);
30482
+ metadataArray = categoryMetadataRef.current;
30483
+ }
29899
30484
  console.log(`[handlePrevious] Unified navigation: ${currentFilter}, metadata index: ${currentMetaIndex}/${metadataArray.length}`);
30485
+ if (metadataArray.length === 0) {
30486
+ console.warn("[handlePrevious] No metadata available after refresh - stopping navigation");
30487
+ clearLoadingState();
30488
+ return;
30489
+ }
29900
30490
  if (currentMetaIndex > 0) {
29901
30491
  const prevMetadataIndex = currentMetaIndex - 1;
29902
30492
  const prevClipMeta = metadataArray[prevMetadataIndex];
@@ -29927,7 +30517,7 @@ var BottlenecksContent = ({
29927
30517
  });
29928
30518
  clearLoadingState();
29929
30519
  }
29930
- }, [clearLoadingState, s3ClipsService]);
30520
+ }, [clearLoadingState, s3ClipsService, loadCategoryMetadata]);
29931
30521
  const currentVideo = React23.useMemo(() => {
29932
30522
  if (!filteredVideos || filteredVideos.length === 0 || currentIndex >= filteredVideos.length) {
29933
30523
  return null;
@@ -30903,7 +31493,7 @@ function DiagnosisVideoModal({
30903
31493
  }
30904
31494
  loadClip();
30905
31495
  }, [clipId, supabase, transformPlaylistUrls]);
30906
- const formatTime3 = (seconds) => {
31496
+ const formatTime4 = (seconds) => {
30907
31497
  const mins = Math.floor(seconds / 60);
30908
31498
  const secs = Math.floor(seconds % 60);
30909
31499
  return `${mins}:${secs.toString().padStart(2, "0")}`;
@@ -31095,9 +31685,9 @@ function DiagnosisVideoModal({
31095
31685
  }
31096
31686
  ),
31097
31687
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm font-medium", children: [
31098
- formatTime3(currentTime),
31688
+ formatTime4(currentTime),
31099
31689
  " / ",
31100
- formatTime3(duration)
31690
+ formatTime4(duration)
31101
31691
  ] }),
31102
31692
  /* @__PURE__ */ jsxRuntime.jsx(
31103
31693
  "input",
@@ -33068,7 +33658,7 @@ var LinePdfGenerator = ({
33068
33658
  }
33069
33659
  hourEndTime.setSeconds(0);
33070
33660
  hourEndTime.setMilliseconds(0);
33071
- const formatTime3 = (date2) => {
33661
+ const formatTime4 = (date2) => {
33072
33662
  return date2.toLocaleTimeString("en-IN", {
33073
33663
  hour: "2-digit",
33074
33664
  minute: "2-digit",
@@ -33076,7 +33666,7 @@ var LinePdfGenerator = ({
33076
33666
  timeZone: "Asia/Kolkata"
33077
33667
  });
33078
33668
  };
33079
- return `${formatTime3(hourStartTime)} - ${formatTime3(hourEndTime)}`;
33669
+ return `${formatTime4(hourStartTime)} - ${formatTime4(hourEndTime)}`;
33080
33670
  });
33081
33671
  };
33082
33672
  const hourlyTimeRanges = lineInfo.metrics.shift_start ? getHourlyTimeRanges(lineInfo.metrics.shift_start, lineInfo.metrics.shift_end) : [];
@@ -39662,7 +40252,7 @@ var AIAgentView = () => {
39662
40252
  }
39663
40253
  return formattedLines.join("");
39664
40254
  };
39665
- const formatTime3 = (timestamp) => {
40255
+ const formatTime4 = (timestamp) => {
39666
40256
  const date = new Date(timestamp);
39667
40257
  return date.toLocaleTimeString([], {
39668
40258
  hour: "2-digit",
@@ -40926,7 +41516,7 @@ var AIAgentView = () => {
40926
41516
  }
40927
41517
  ),
40928
41518
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `mt-1.5 sm:mt-2 flex items-center gap-2 text-xs text-gray-400 ${message.role === "user" ? "justify-end" : "justify-start"}`, children: [
40929
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: formatTime3(message.created_at) }),
41519
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: formatTime4(message.created_at) }),
40930
41520
  message.role === "assistant" && message.id !== -1 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
40931
41521
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1 h-1 bg-gray-300 rounded-full" }),
40932
41522
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Axel" })