@scalemule/nextjs 0.1.24 → 0.1.26

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
@@ -580,6 +580,7 @@ var ScaleMuleClient = class {
580
580
  this.anonymousId = null;
581
581
  this.sessionPool = /* @__PURE__ */ new Map();
582
582
  this.knownAccounts = /* @__PURE__ */ new Map();
583
+ this.refreshPromise = null;
583
584
  this.apiKey = config.apiKey;
584
585
  this.applicationId = config.applicationId || null;
585
586
  this.baseUrl = config.baseUrl || GATEWAY_URLS[config.environment || "prod"];
@@ -598,6 +599,9 @@ var ScaleMuleClient = class {
598
599
  this.multiSessionEnabled = config.enableMultiSession || false;
599
600
  this.accountSwitcherEnabled = config.enableAccountSwitcher || false;
600
601
  this.accountSwitcherPrivacy = config.accountSwitcherPrivacy || "full";
602
+ this.onRefreshStart = config.onRefreshStart;
603
+ this.onRefreshEnd = config.onRefreshEnd;
604
+ this.onAutoRefreshFailed = config.onAutoRefreshFailed;
601
605
  }
602
606
  // --------------------------------------------------------------------------
603
607
  // Session Management
@@ -883,34 +887,28 @@ var ScaleMuleClient = class {
883
887
  const method = (init.method || "GET").toUpperCase();
884
888
  const timeout = init.timeout || this.defaultTimeout;
885
889
  const maxRetries = init.skipRetry ? 0 : init.retries ?? this.maxRetries;
886
- const headers = {
887
- "x-api-key": this.apiKey,
888
- "User-Agent": `ScaleMule-SDK-TypeScript/${SDK_VERSION}`,
889
- ...init.headers
890
- };
891
- if (!init.skipAuth && this.sessionToken) {
892
- headers["Authorization"] = `Bearer ${this.sessionToken}`;
893
- }
894
- if (this.workspaceId) {
895
- headers["x-sm-workspace-id"] = this.workspaceId;
896
- }
897
- if (!this.sessionToken && this.anonymousId) {
898
- headers["x-anonymous-id"] = this.anonymousId;
899
- }
900
- let bodyStr;
901
- if (init.body !== void 0 && init.body !== null) {
902
- bodyStr = typeof init.body === "string" ? init.body : JSON.stringify(init.body);
903
- if (!headers["Content-Type"]) {
904
- headers["Content-Type"] = "application/json";
905
- }
906
- }
907
- if (this.debug) {
908
- console.log(`[ScaleMule] ${method} ${path}`);
909
- }
910
- const idempotencyKey = method === "POST" ? generateIdempotencyKey() : void 0;
890
+ const bodyStr = init.body ? JSON.stringify(init.body) : void 0;
911
891
  let lastError = null;
892
+ let idempotencyKey = null;
912
893
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
913
- if (attempt > 0 && idempotencyKey) {
894
+ const headers = {
895
+ "x-api-key": this.apiKey,
896
+ "User-Agent": `ScaleMule-SDK-TypeScript/${SDK_VERSION}`,
897
+ ...init.headers
898
+ };
899
+ if (!init.skipAuth && this.sessionToken) {
900
+ headers["Authorization"] = `Bearer ${this.sessionToken}`;
901
+ }
902
+ if (this.workspaceId) {
903
+ headers["x-sm-workspace-id"] = this.workspaceId;
904
+ }
905
+ if (!this.sessionToken && this.anonymousId) {
906
+ headers["x-anonymous-id"] = this.anonymousId;
907
+ }
908
+ if (attempt > 0 && (method === "POST" || method === "PUT" || method === "PATCH")) {
909
+ if (!idempotencyKey) {
910
+ idempotencyKey = generateIdempotencyKey();
911
+ }
914
912
  headers["x-idempotency-key"] = idempotencyKey;
915
913
  }
916
914
  const controller = new AbortController();
@@ -930,6 +928,10 @@ var ScaleMuleClient = class {
930
928
  signal: controller.signal
931
929
  });
932
930
  clearTimeout(timeoutId);
931
+ const rotatedToken = response.headers.get("x-rotated-session-token");
932
+ if (rotatedToken && this.sessionToken && this.userId) {
933
+ this.setSession(rotatedToken, this.userId);
934
+ }
933
935
  if (this.rateLimitQueue) {
934
936
  this.rateLimitQueue.updateFromHeaders(response.headers);
935
937
  }
@@ -952,6 +954,45 @@ var ScaleMuleClient = class {
952
954
  status: response.status,
953
955
  details: responseData?.error?.details || responseData?.details
954
956
  };
957
+ if (response.status === 401 && this.sessionToken && !init.isAutoRefresh) {
958
+ if (this.debug) console.log("[ScaleMule] 401 received, attempting auto-refresh...");
959
+ let isPrimaryRefresh = false;
960
+ if (!this.refreshPromise) {
961
+ isPrimaryRefresh = true;
962
+ this.onRefreshStart?.();
963
+ this.refreshPromise = this.auth?.refreshSession ? this.auth.refreshSession() : this.post("/auth/refresh", {}, { isAutoRefresh: true });
964
+ }
965
+ const currentRefreshPromise = this.refreshPromise;
966
+ try {
967
+ const refreshResult = await currentRefreshPromise;
968
+ if (!refreshResult || refreshResult.error) {
969
+ if (this.debug) console.log("[ScaleMule] Auto-refresh failed:", refreshResult?.error);
970
+ const apiError = refreshResult?.error || {
971
+ code: "refresh_failed",
972
+ message: "Auto-refresh failed",
973
+ status: 400
974
+ };
975
+ init.onAutoRefreshFailed?.(apiError);
976
+ this.onAutoRefreshFailed?.(apiError);
977
+ return { data: null, error };
978
+ }
979
+ if (this.debug) console.log("[ScaleMule] Auto-refresh succeeded, retrying original request...");
980
+ const newToken = refreshResult.data?.session_token || refreshResult.data?.access_token;
981
+ if (newToken) {
982
+ this.sessionToken = newToken;
983
+ if (this.userId) {
984
+ await this.storage.setItem(SESSION_STORAGE_KEY, newToken);
985
+ }
986
+ }
987
+ attempt--;
988
+ continue;
989
+ } finally {
990
+ if (isPrimaryRefresh && this.refreshPromise === currentRefreshPromise) {
991
+ this.refreshPromise = null;
992
+ this.onRefreshEnd?.();
993
+ }
994
+ }
995
+ }
955
996
  if (response.status === 429) {
956
997
  const retryAfter = response.headers.get("Retry-After");
957
998
  if (retryAfter) {
@@ -5735,7 +5776,7 @@ var AudioService = class extends ServiceModule {
5735
5776
  constructor(client, storage) {
5736
5777
  super(client);
5737
5778
  this.storage = storage;
5738
- this.basePath = "/v1/audios";
5779
+ this.basePath = "/v1/audio";
5739
5780
  }
5740
5781
  /**
5741
5782
  * Register a storage-uploaded audio asset with the audio service.
@@ -5792,6 +5833,32 @@ var AudioService = class extends ServiceModule {
5792
5833
  };
5793
5834
  }
5794
5835
  };
5836
+ var TtsService = class extends ServiceModule {
5837
+ constructor() {
5838
+ super(...arguments);
5839
+ this.basePath = "/v1/tts";
5840
+ }
5841
+ async synthesize(params, options) {
5842
+ return this.post(
5843
+ "/synthesize",
5844
+ {
5845
+ text: params.text,
5846
+ voice: params.voice,
5847
+ model: params.model,
5848
+ provider: params.provider,
5849
+ async: params.async,
5850
+ access_mode: params.accessMode
5851
+ },
5852
+ options
5853
+ );
5854
+ }
5855
+ async getJob(id, options) {
5856
+ return this._get(`/jobs/${id}`, options);
5857
+ }
5858
+ async listVoices(params, options) {
5859
+ return this._get(this.withQuery("/voices", { provider: params?.provider }), options);
5860
+ }
5861
+ };
5795
5862
  var DeadLetterApi = class extends ServiceModule {
5796
5863
  constructor() {
5797
5864
  super(...arguments);
@@ -6612,6 +6679,7 @@ var ScaleMule = class {
6612
6679
  this.functions = new FunctionsService(this._client);
6613
6680
  this.photo = new PhotoService(this._client, this.storage);
6614
6681
  this.audio = new AudioService(this._client, this.storage);
6682
+ this.tts = new TtsService(this._client);
6615
6683
  this.flagContent = new FlagContentService(this._client);
6616
6684
  this.creatorMaker = new CreatorMakerService(this._client);
6617
6685
  this.compliance = new ComplianceService(this._client);
@@ -7178,6 +7246,24 @@ var ScaleMuleClient2 = class {
7178
7246
  console.log("[ScaleMule] Session set for user:", userId);
7179
7247
  }
7180
7248
  }
7249
+ /**
7250
+ * Token-only setter for member-auth surfaces.
7251
+ *
7252
+ * Unlike `setSession(token, userId)`, this does NOT persist a userId or
7253
+ * write to local storage — useful when the host platform owns identity
7254
+ * (e.g. ScaleMule's member dashboards use `MemberAuthProvider` and the
7255
+ * token comes from a `${env}_member_access_token` cookie). The token is
7256
+ * applied in-memory and used as a `Bearer` Authorization header on
7257
+ * subsequent requests.
7258
+ *
7259
+ * Pass `null` to clear without touching userId/storage.
7260
+ */
7261
+ setSessionToken(token) {
7262
+ this.sessionToken = token;
7263
+ if (this.debug) {
7264
+ console.log("[ScaleMule] Session token", token ? "set (token-only)" : "cleared (token-only)");
7265
+ }
7266
+ }
7181
7267
  /**
7182
7268
  * Clear session on logout
7183
7269
  */
@@ -7242,6 +7328,8 @@ var ScaleMuleClient2 = class {
7242
7328
  console.log(`[ScaleMule] ${options.method || "GET"} ${path}`);
7243
7329
  }
7244
7330
  let lastError = null;
7331
+ const MAX_REFRESH_ATTEMPTS = 1;
7332
+ let refreshAttempts = 0;
7245
7333
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
7246
7334
  const controller = new AbortController();
7247
7335
  const timeoutId = setTimeout(() => controller.abort(), timeout);
@@ -7265,7 +7353,8 @@ var ScaleMuleClient2 = class {
7265
7353
  if (!response.ok) {
7266
7354
  const rawError = responseData?.error;
7267
7355
  const error = rawError && typeof rawError === "object" ? rawError : { code: `HTTP_${response.status}`, message: typeof rawError === "string" ? rawError : responseData?.message || text || response.statusText };
7268
- if (response.status === 401 && this.sessionToken && !options.isAutoRefresh) {
7356
+ if (response.status === 401 && this.sessionToken && !options.isAutoRefresh && refreshAttempts < MAX_REFRESH_ATTEMPTS) {
7357
+ refreshAttempts++;
7269
7358
  if (this.debug) console.log("[ScaleMule] 401 received, attempting auto-refresh...");
7270
7359
  if (!this.refreshPromise) {
7271
7360
  this.onRefreshStart?.();
@@ -7634,8 +7723,12 @@ function ScaleMuleProvider({
7634
7723
  onLogout,
7635
7724
  onAuthError,
7636
7725
  bootstrapFlags,
7637
- mediaPolicy
7726
+ mediaPolicy,
7727
+ getToken,
7728
+ userResolver,
7729
+ memberTokenPollMs
7638
7730
  }) {
7731
+ const memberMode = typeof getToken === "function";
7639
7732
  const [user, setUser] = React.useState(null);
7640
7733
  const [initializing, setInitializing] = React.useState(true);
7641
7734
  const [error, setError] = React.useState(null);
@@ -7652,9 +7745,13 @@ function ScaleMuleProvider({
7652
7745
  gatewayUrl: resolvedGatewayUrl,
7653
7746
  debug,
7654
7747
  storage,
7655
- pendingSessionInit: !!authProxyUrl
7748
+ // Make outbound API calls wait for the first token to land in any
7749
+ // mode where the provider is responsible for populating the
7750
+ // session asynchronously: auth-proxy fetch, or member-mode
7751
+ // getToken() callback. Resolved in the init effect below.
7752
+ pendingSessionInit: !!authProxyUrl || memberMode
7656
7753
  }),
7657
- [apiKey, applicationId, environment, resolvedGatewayUrl, debug, storage, authProxyUrl]
7754
+ [apiKey, applicationId, environment, resolvedGatewayUrl, debug, storage, authProxyUrl, memberMode]
7658
7755
  );
7659
7756
  const money$1 = React.useMemo(
7660
7757
  () => money.createMoneyClient({
@@ -7676,8 +7773,10 @@ function ScaleMuleProvider({
7676
7773
  });
7677
7774
  }, [apiKey, applicationId, environment, resolvedGatewayUrl, debug]);
7678
7775
  const [fetchedPolicy, setFetchedPolicy] = React.useState(void 0);
7776
+ const [tokenVersion, setTokenVersion] = React.useState(0);
7679
7777
  React.useEffect(() => {
7680
7778
  if (mediaPolicy) return;
7779
+ if (memberMode && tokenVersion === 0) return;
7681
7780
  let mounted = true;
7682
7781
  void (async () => {
7683
7782
  try {
@@ -7695,7 +7794,7 @@ function ScaleMuleProvider({
7695
7794
  return () => {
7696
7795
  mounted = false;
7697
7796
  };
7698
- }, [baseClient, mediaPolicy]);
7797
+ }, [baseClient, mediaPolicy, memberMode, tokenVersion]);
7699
7798
  const effectiveMediaPolicy = mediaPolicy ?? fetchedPolicy;
7700
7799
  React.useEffect(() => {
7701
7800
  const token = client.getSessionToken();
@@ -7713,6 +7812,43 @@ function ScaleMuleProvider({
7713
7812
  try {
7714
7813
  await client.initialize();
7715
7814
  const cachedUser = getCachedUser();
7815
+ if (memberMode) {
7816
+ try {
7817
+ const token = await Promise.resolve(getToken()) ?? null;
7818
+ if (mounted) {
7819
+ if (token) {
7820
+ client.setSessionToken(token);
7821
+ baseClient.setAccessToken(token);
7822
+ money$1.setAccessToken(token);
7823
+ } else {
7824
+ client.setSessionToken(null);
7825
+ baseClient.clearAccessToken();
7826
+ money$1.setAccessToken(void 0);
7827
+ }
7828
+ setTokenVersion((v) => v + 1);
7829
+ }
7830
+ if (userResolver) {
7831
+ try {
7832
+ const resolvedUser = await userResolver();
7833
+ if (mounted) {
7834
+ setUser(resolvedUser);
7835
+ setCachedUser(resolvedUser);
7836
+ }
7837
+ } catch (resolveErr) {
7838
+ if (mounted && debug) {
7839
+ console.debug("[ScaleMule] userResolver() failed:", resolveErr);
7840
+ }
7841
+ }
7842
+ }
7843
+ } catch (memberErr) {
7844
+ if (mounted && debug) {
7845
+ console.debug("[ScaleMule] getToken() failed:", memberErr);
7846
+ }
7847
+ } finally {
7848
+ client.resolveSessionPending();
7849
+ }
7850
+ return;
7851
+ }
7716
7852
  if (authProxyUrl) {
7717
7853
  if (cachedUser && mounted) {
7718
7854
  setUser(cachedUser);
@@ -7784,7 +7920,43 @@ function ScaleMuleProvider({
7784
7920
  return () => {
7785
7921
  mounted = false;
7786
7922
  };
7787
- }, [client, debug, onAuthError, authProxyUrl]);
7923
+ }, [client, baseClient, money$1, debug, onAuthError, authProxyUrl, memberMode, getToken, userResolver]);
7924
+ React.useEffect(() => {
7925
+ if (!memberMode) return;
7926
+ if (memberTokenPollMs === null) return;
7927
+ const interval = memberTokenPollMs ?? 6e4;
7928
+ if (interval <= 0) return;
7929
+ let cancelled = false;
7930
+ const id = setInterval(async () => {
7931
+ try {
7932
+ const token = await Promise.resolve(getToken()) ?? null;
7933
+ if (cancelled) return;
7934
+ const current = client.getSessionToken();
7935
+ if (token === current) return;
7936
+ if (token) {
7937
+ client.setSessionToken(token);
7938
+ baseClient.setAccessToken(token);
7939
+ money$1.setAccessToken(token);
7940
+ } else {
7941
+ client.setSessionToken(null);
7942
+ baseClient.clearAccessToken();
7943
+ money$1.setAccessToken(void 0);
7944
+ if (debug) {
7945
+ console.debug("[ScaleMule] Member token cleared on refresh");
7946
+ }
7947
+ }
7948
+ setTokenVersion((v) => v + 1);
7949
+ } catch (err) {
7950
+ if (debug) {
7951
+ console.debug("[ScaleMule] Member token refresh failed:", err);
7952
+ }
7953
+ }
7954
+ }, interval);
7955
+ return () => {
7956
+ cancelled = true;
7957
+ clearInterval(id);
7958
+ };
7959
+ }, [memberMode, memberTokenPollMs, getToken, client, baseClient, money$1, debug]);
7788
7960
  const handleSetUser = React.useCallback(
7789
7961
  (newUser) => {
7790
7962
  setUser(newUser);
@@ -8266,8 +8438,8 @@ function useAuth() {
8266
8438
  const getLinkedAccounts = React.useCallback(async () => {
8267
8439
  setError(null);
8268
8440
  try {
8269
- const data = await client.get("/v1/auth/oauth/accounts");
8270
- return data.accounts;
8441
+ const data = await client.get("/v1/auth/oauth/providers");
8442
+ return data.providers;
8271
8443
  } catch (err) {
8272
8444
  if (err instanceof ScaleMuleApiError) {
8273
8445
  setError(err);
@@ -8276,41 +8448,30 @@ function useAuth() {
8276
8448
  }
8277
8449
  }, [client, setError]);
8278
8450
  const linkAccount = React.useCallback(
8279
- async (config) => {
8451
+ async (_config) => {
8280
8452
  setError(null);
8281
8453
  if (!user) {
8282
- const err = {
8454
+ const err2 = {
8283
8455
  code: "NOT_AUTHENTICATED",
8284
8456
  message: "Must be logged in to link accounts"
8285
8457
  };
8286
- setError(err);
8287
- throw err;
8288
- }
8289
- let linkData;
8290
- try {
8291
- linkData = await client.post("/v1/auth/oauth/link", {
8292
- provider: config.provider,
8293
- redirect_url: config.redirectUrl,
8294
- scopes: config.scopes
8295
- });
8296
- } catch (err) {
8297
- if (err instanceof ScaleMuleApiError) {
8298
- setError(err);
8299
- }
8300
- throw err;
8301
- }
8302
- if (typeof sessionStorage !== "undefined") {
8303
- sessionStorage.setItem("scalemule_oauth_state", linkData.state);
8458
+ setError(err2);
8459
+ throw err2;
8304
8460
  }
8305
- return linkData;
8461
+ const err = {
8462
+ code: "NOT_IMPLEMENTED",
8463
+ message: "OAuth account linking from a logged-in session is not yet supported. Use the standard OAuth sign-in flow instead."
8464
+ };
8465
+ setError(err);
8466
+ throw err;
8306
8467
  },
8307
- [client, user, setError]
8468
+ [user, setError]
8308
8469
  );
8309
8470
  const unlinkAccount = React.useCallback(
8310
8471
  async (provider) => {
8311
8472
  setError(null);
8312
8473
  try {
8313
- await client.delete(`/v1/auth/oauth/accounts/${provider}`);
8474
+ await client.delete(`/v1/auth/oauth/providers/${encodeURIComponent(provider)}`);
8314
8475
  } catch (err) {
8315
8476
  if (err instanceof ScaleMuleApiError) {
8316
8477
  setError(err);
@@ -10920,6 +11081,75 @@ function useFeedback(options = {}) {
10920
11081
  );
10921
11082
  return { items, loading, error, submit, refresh };
10922
11083
  }
11084
+ var ZERO = { value: 0, up_count: 0, down_count: 0, score: 0 };
11085
+ function useVote({
11086
+ targetType,
11087
+ targetId,
11088
+ initialState,
11089
+ refetchOnMount = false,
11090
+ enabled = true
11091
+ }) {
11092
+ const { client } = useScaleMule();
11093
+ const [state, setState] = React.useState(initialState ?? ZERO);
11094
+ const [isLoading, setIsLoading] = React.useState(!initialState && enabled);
11095
+ const [error, setError] = React.useState(null);
11096
+ const inFlight = React.useRef(null);
11097
+ const path = `/v1/social/${encodeURIComponent(targetType)}/${encodeURIComponent(targetId)}/vote`;
11098
+ const refetch = React.useCallback(async () => {
11099
+ if (!enabled) return;
11100
+ setIsLoading(true);
11101
+ setError(null);
11102
+ try {
11103
+ const data = await client.get(path);
11104
+ setState(data);
11105
+ } catch (e) {
11106
+ setError(e);
11107
+ } finally {
11108
+ setIsLoading(false);
11109
+ }
11110
+ }, [client, path, enabled]);
11111
+ React.useEffect(() => {
11112
+ if (!enabled) return;
11113
+ if (initialState && !refetchOnMount) return;
11114
+ void refetch();
11115
+ }, [enabled, refetch, initialState, refetchOnMount]);
11116
+ const cast = React.useCallback(
11117
+ async (value) => {
11118
+ const prev = state;
11119
+ const optimistic = applyVote(prev, value);
11120
+ setState(optimistic);
11121
+ setError(null);
11122
+ const send = async () => {
11123
+ try {
11124
+ const data = await client.put(path, { value });
11125
+ setState(data);
11126
+ } catch (e) {
11127
+ setState(prev);
11128
+ setError(e);
11129
+ }
11130
+ };
11131
+ const p = inFlight.current ? inFlight.current.then(send) : send();
11132
+ inFlight.current = p;
11133
+ await p;
11134
+ if (inFlight.current === p) inFlight.current = null;
11135
+ },
11136
+ [client, path, state]
11137
+ );
11138
+ return { state, isLoading, error, cast, refetch };
11139
+ }
11140
+ function applyVote(prev, next) {
11141
+ let { up_count, down_count } = prev;
11142
+ if (prev.value === 1) up_count = Math.max(0, up_count - 1);
11143
+ if (prev.value === -1) down_count = Math.max(0, down_count - 1);
11144
+ if (next === 1) up_count += 1;
11145
+ if (next === -1) down_count += 1;
11146
+ return {
11147
+ value: next,
11148
+ up_count,
11149
+ down_count,
11150
+ score: up_count - down_count
11151
+ };
11152
+ }
10923
11153
  var TYPE_LABELS = {
10924
11154
  bug_report: "Bug",
10925
11155
  feature_request: "Feature request",
@@ -11288,449 +11518,128 @@ function FeedbackWidget(props) {
11288
11518
  ) })
11289
11519
  ] });
11290
11520
  }
11291
- var SPEEDS = [1, 1.25, 1.5, 2];
11292
- var EXPIRY_REFRESH_WINDOW_MS = 3e4;
11293
- var styles = {
11521
+ var ARROW_UP = "\u25B2";
11522
+ var ARROW_DOWN = "\u25BC";
11523
+ var STYLES2 = {
11294
11524
  root: {
11295
- borderRadius: "1.75rem",
11296
- border: "1px solid rgba(226, 232, 240, 0.8)",
11297
- background: "linear-gradient(180deg, #f8fbff 0%, #edf4ff 100%)",
11298
- padding: "1rem",
11299
- color: "#0f172b",
11300
- boxShadow: "0 22px 70px rgba(4, 12, 24, 0.28)"
11301
- },
11302
- layout: {
11303
- display: "flex",
11304
- flexWrap: "wrap",
11305
- alignItems: "center",
11306
- gap: "1rem"
11307
- },
11308
- playButton: {
11309
- display: "flex",
11310
- height: "4.5rem",
11311
- width: "4.5rem",
11312
- flexShrink: 0,
11525
+ display: "inline-flex",
11313
11526
  alignItems: "center",
11314
- justifyContent: "center",
11315
- borderRadius: "9999px",
11316
- border: "none",
11317
- backgroundColor: "#020618",
11318
- color: "#ffffff",
11319
- boxShadow: "0 10px 28px rgba(15, 23, 42, 0.25)",
11320
- cursor: "pointer"
11321
- },
11322
- playIcon: {
11323
- display: "block",
11324
- width: "1.75rem",
11325
- height: "1.75rem",
11326
- fill: "currentColor"
11327
- },
11328
- playIconOffset: {
11329
- marginLeft: "0.25rem"
11330
- },
11331
- content: {
11332
- minWidth: "16rem",
11333
- flex: "1 1 18rem"
11527
+ gap: 6
11334
11528
  },
11335
- metaRow: {
11336
- display: "flex",
11337
- alignItems: "center",
11338
- justifyContent: "space-between",
11339
- gap: "0.75rem",
11340
- fontSize: "0.8rem",
11341
- fontWeight: 500,
11342
- color: "#62748e"
11343
- },
11344
- metaRight: {
11345
- display: "flex",
11529
+ rootStacked: {
11530
+ display: "inline-flex",
11531
+ flexDirection: "column",
11346
11532
  alignItems: "center",
11347
- gap: "0.75rem"
11533
+ gap: 2
11348
11534
  },
11349
- badge: {
11350
- borderRadius: "9999px",
11351
- backgroundColor: "#dbeafe",
11352
- padding: "0.25rem 0.75rem",
11353
- fontSize: "0.65rem",
11354
- fontWeight: 600,
11355
- letterSpacing: "0.24em",
11356
- textTransform: "uppercase",
11357
- color: "#1447e6"
11358
- },
11359
- scrubber: {
11360
- position: "relative",
11361
- marginTop: "1rem",
11362
- height: "3rem",
11535
+ button: {
11536
+ border: "1px solid rgba(255,255,255,0.12)",
11537
+ background: "rgba(255,255,255,0.04)",
11538
+ color: "inherit",
11363
11539
  cursor: "pointer",
11364
- overflow: "hidden",
11365
- borderRadius: "1rem",
11366
- backgroundColor: "rgba(226, 232, 240, 0.9)",
11367
- paddingInline: "0.25rem"
11368
- },
11369
- waveform: {
11370
- pointerEvents: "none",
11371
- position: "absolute",
11372
- inset: 0,
11373
- height: "100%",
11374
- width: "100%"
11375
- },
11376
- fallbackTrack: {
11377
- position: "absolute",
11378
- inset: 0,
11379
- backgroundColor: "rgba(226, 232, 240, 0.9)"
11380
- },
11381
- fallbackProgress: {
11382
- position: "absolute",
11383
- insetBlock: 0,
11384
- left: 0,
11385
- backgroundColor: "#155dfc"
11540
+ borderRadius: 9999,
11541
+ padding: "6px 10px",
11542
+ lineHeight: 1,
11543
+ fontSize: 14
11386
11544
  },
11387
- chipsRow: {
11388
- marginTop: "1rem",
11389
- display: "flex",
11390
- flexWrap: "wrap",
11391
- alignItems: "center",
11392
- gap: "0.5rem",
11393
- fontSize: "0.72rem",
11394
- fontWeight: 500,
11395
- color: "#62748e"
11396
- },
11397
- chipStrong: {
11398
- borderRadius: "9999px",
11399
- border: "1px solid #e2e8f0",
11400
- backgroundColor: "rgba(255, 255, 255, 0.85)",
11401
- padding: "0.25rem 0.75rem",
11402
- color: "#334155"
11403
- },
11404
- chipMuted: {
11405
- borderRadius: "9999px",
11406
- border: "1px solid #e2e8f0",
11407
- backgroundColor: "rgba(255, 255, 255, 0.65)",
11408
- padding: "0.25rem 0.75rem"
11409
- },
11410
- controls: {
11411
- display: "flex",
11412
- flexShrink: 0,
11413
- alignItems: "center",
11414
- gap: "0.5rem",
11415
- alignSelf: "flex-start"
11545
+ buttonCompact: {
11546
+ padding: "3px 7px",
11547
+ fontSize: 12
11416
11548
  },
11417
- secondaryButton: {
11418
- borderRadius: "9999px",
11419
- border: "1px solid #cad5e2",
11420
- backgroundColor: "rgba(255, 255, 255, 0.9)",
11421
- padding: "0.5rem 1rem",
11422
- fontSize: "1rem",
11423
- fontWeight: 600,
11424
- color: "#334155",
11425
- boxShadow: "0 1px 3px rgba(15, 23, 42, 0.08)",
11426
- cursor: "pointer"
11549
+ buttonActiveUp: {
11550
+ background: "rgba(34,197,94,0.18)",
11551
+ borderColor: "rgba(34,197,94,0.45)",
11552
+ color: "#86efac"
11427
11553
  },
11428
- refreshButton: {
11429
- borderRadius: "9999px",
11430
- border: "1px solid #cad5e2",
11431
- backgroundColor: "rgba(255, 255, 255, 0.75)",
11432
- padding: "0.5rem 1rem",
11433
- fontSize: "0.875rem",
11434
- fontWeight: 600,
11435
- color: "#334155",
11436
- cursor: "pointer"
11554
+ buttonActiveDown: {
11555
+ background: "rgba(239,68,68,0.18)",
11556
+ borderColor: "rgba(239,68,68,0.45)",
11557
+ color: "#fca5a5"
11437
11558
  },
11438
- disabled: {
11439
- cursor: "not-allowed",
11440
- opacity: 0.6
11559
+ score: {
11560
+ minWidth: 24,
11561
+ textAlign: "center",
11562
+ fontVariantNumeric: "tabular-nums",
11563
+ fontWeight: 600
11441
11564
  }
11442
11565
  };
11443
- function formatTime(seconds) {
11444
- if (!Number.isFinite(seconds) || seconds <= 0) {
11445
- return "0:00";
11446
- }
11447
- const wholeSeconds = Math.floor(seconds);
11448
- const minutes = Math.floor(wholeSeconds / 60);
11449
- const remainder = wholeSeconds % 60;
11450
- return `${minutes}:${remainder.toString().padStart(2, "0")}`;
11451
- }
11452
- function getNextSpeed(currentSpeed) {
11453
- const currentIndex = SPEEDS.findIndex((speed) => speed === currentSpeed);
11454
- if (currentIndex === -1) {
11455
- return SPEEDS[0];
11456
- }
11457
- return SPEEDS[(currentIndex + 1) % SPEEDS.length];
11458
- }
11459
- function NarrationPlayer({
11460
- audio,
11461
- className,
11462
- providerLabel = "ScaleMule",
11463
- narrationLabel = "AI-Narrated",
11464
- refreshing = false,
11465
- onRefresh,
11466
- showRefreshButton = false,
11467
- onPlaybackError
11566
+ function VoteButton({
11567
+ targetType,
11568
+ targetId,
11569
+ initialState,
11570
+ layout = "horizontal",
11571
+ size = "regular",
11572
+ onSignInRequired,
11573
+ onError,
11574
+ classNames,
11575
+ upLabel = "Upvote",
11576
+ downLabel = "Downvote"
11468
11577
  }) {
11469
- const audioRef = React.useRef(null);
11470
- const scrubRef = React.useRef(null);
11471
- const pendingResumeRef = React.useRef(false);
11472
- const pendingResumeTimeRef = React.useRef(null);
11473
- const recoveredUrlRef = React.useRef(null);
11474
- const [playing, setPlaying] = React.useState(false);
11475
- const [currentTime, setCurrentTime] = React.useState(0);
11476
- const [duration, setDuration] = React.useState(audio.duration_ms ? audio.duration_ms / 1e3 : 0);
11477
- const [speed, setSpeed] = React.useState(1.5);
11478
- const peaks = React.useMemo(() => {
11479
- if (!Array.isArray(audio.waveform_peaks)) {
11480
- return [];
11481
- }
11482
- const sampled = audio.waveform_peaks.filter((_, index) => index % 3 === 0).slice(0, 220);
11483
- return sampled.length > 0 ? sampled : audio.waveform_peaks.slice(0, 220);
11484
- }, [audio.waveform_peaks]);
11485
- React.useEffect(() => {
11486
- const element = audioRef.current;
11487
- if (!element) return;
11488
- recoveredUrlRef.current = null;
11489
- const resumeTime = pendingResumeTimeRef.current;
11490
- setCurrentTime(resumeTime ?? 0);
11491
- setPlaying(false);
11492
- element.pause();
11493
- try {
11494
- element.currentTime = resumeTime ?? 0;
11495
- } catch {
11496
- element.currentTime = 0;
11497
- }
11498
- if (pendingResumeRef.current) {
11499
- pendingResumeRef.current = false;
11500
- pendingResumeTimeRef.current = null;
11501
- element.play().catch(() => {
11502
- setPlaying(false);
11503
- onPlaybackError?.();
11504
- });
11505
- return;
11506
- }
11507
- pendingResumeTimeRef.current = null;
11508
- }, [audio.url]);
11509
- React.useEffect(() => {
11510
- const element = audioRef.current;
11511
- if (!element) return;
11512
- element.playbackRate = speed;
11513
- element.preservesPitch = true;
11514
- }, [speed]);
11515
- function isUrlExpiringSoon() {
11516
- if (!audio.expires_at) {
11517
- return false;
11518
- }
11519
- const expiresAtMs = Date.parse(audio.expires_at);
11520
- if (!Number.isFinite(expiresAtMs)) {
11521
- return false;
11522
- }
11523
- return expiresAtMs - Date.now() <= EXPIRY_REFRESH_WINDOW_MS;
11524
- }
11525
- async function refreshAndResume() {
11526
- if (!onRefresh || refreshing) {
11527
- return false;
11528
- }
11529
- if (recoveredUrlRef.current === (audio.url ?? null)) {
11530
- return false;
11531
- }
11532
- recoveredUrlRef.current = audio.url ?? null;
11533
- pendingResumeRef.current = true;
11534
- pendingResumeTimeRef.current = currentTime;
11535
- try {
11536
- await Promise.resolve(onRefresh());
11537
- return true;
11538
- } catch {
11539
- pendingResumeRef.current = false;
11540
- pendingResumeTimeRef.current = null;
11541
- onPlaybackError?.();
11542
- return false;
11543
- }
11544
- }
11545
- function togglePlayback() {
11546
- const element = audioRef.current;
11547
- if (!element) return;
11548
- if (element.paused) {
11549
- if (refreshing) {
11550
- pendingResumeRef.current = true;
11551
- pendingResumeTimeRef.current = currentTime;
11578
+ const { isAuthenticated } = useAuth();
11579
+ const { state, cast, error } = useVote({
11580
+ targetType,
11581
+ targetId,
11582
+ initialState: initialState ?? null,
11583
+ enabled: isAuthenticated
11584
+ });
11585
+ const handleVote = React.useCallback(
11586
+ async (next) => {
11587
+ if (!isAuthenticated) {
11588
+ onSignInRequired?.();
11552
11589
  return;
11553
11590
  }
11554
- if (onRefresh && isUrlExpiringSoon()) {
11555
- void refreshAndResume();
11556
- return;
11591
+ const desired = state.value === next ? 0 : next;
11592
+ try {
11593
+ await cast(desired);
11594
+ } catch (e) {
11595
+ onError?.(e);
11557
11596
  }
11558
- element.play().catch(() => {
11559
- setPlaying(false);
11560
- onPlaybackError?.();
11561
- });
11562
- return;
11597
+ },
11598
+ [cast, isAuthenticated, onSignInRequired, onError, state.value]
11599
+ );
11600
+ React.useEffect(() => {
11601
+ if (error) onError?.(error);
11602
+ }, [error, onError]);
11603
+ const compactStyle = size === "compact" ? STYLES2.buttonCompact : {};
11604
+ const upActive = state.value === 1;
11605
+ const downActive = state.value === -1;
11606
+ const upBtn = /* @__PURE__ */ jsxRuntime.jsx(
11607
+ "button",
11608
+ {
11609
+ type: "button",
11610
+ "aria-label": upLabel,
11611
+ "aria-pressed": upActive,
11612
+ title: isAuthenticated ? upLabel : "Sign in to vote",
11613
+ className: [classNames?.upButton, upActive ? classNames?.upButtonActive : void 0].filter(Boolean).join(" ") || void 0,
11614
+ onClick: () => void handleVote(1),
11615
+ style: classNames?.upButton ? void 0 : { ...STYLES2.button, ...compactStyle, ...upActive ? STYLES2.buttonActiveUp : {} },
11616
+ children: ARROW_UP
11563
11617
  }
11564
- element.pause();
11565
- }
11566
- function seekTo(clientX) {
11567
- const element = audioRef.current;
11568
- const scrubber = scrubRef.current;
11569
- if (!element || !scrubber || !duration) return;
11570
- const bounds = scrubber.getBoundingClientRect();
11571
- const ratio = Math.max(0, Math.min(1, (clientX - bounds.left) / bounds.width));
11572
- const nextTime = ratio * duration;
11573
- element.currentTime = nextTime;
11574
- setCurrentTime(nextTime);
11575
- }
11576
- const progress = duration > 0 ? Math.min(1, currentTime / duration) : 0;
11577
- const expiresLabel = audio.expires_at ? new Date(audio.expires_at).toLocaleTimeString([], {
11578
- hour: "numeric",
11579
- minute: "2-digit"
11580
- }) : null;
11581
- if (!audio.url) {
11582
- return null;
11583
- }
11584
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: styles.root, children: [
11585
- /* @__PURE__ */ jsxRuntime.jsx(
11586
- "audio",
11587
- {
11588
- ref: audioRef,
11589
- src: audio.url ?? void 0,
11590
- preload: "metadata",
11591
- onLoadedMetadata: (event) => {
11592
- setDuration(event.currentTarget.duration || 0);
11593
- },
11594
- onTimeUpdate: (event) => {
11595
- setCurrentTime(event.currentTarget.currentTime);
11596
- },
11597
- onPlay: () => setPlaying(true),
11598
- onPause: () => setPlaying(false),
11599
- onEnded: () => setPlaying(false),
11600
- onError: () => {
11601
- if (onRefresh) {
11602
- void refreshAndResume().then((didRefresh) => {
11603
- if (!didRefresh) {
11604
- onPlaybackError?.();
11605
- }
11606
- });
11607
- return;
11608
- }
11609
- onPlaybackError?.();
11610
- }
11611
- }
11612
- ),
11613
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.layout, children: [
11614
- /* @__PURE__ */ jsxRuntime.jsx(
11615
- "button",
11616
- {
11617
- type: "button",
11618
- onClick: togglePlayback,
11619
- "aria-label": playing ? "Pause narration" : "Play narration",
11620
- style: styles.playButton,
11621
- children: playing ? /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", style: styles.playIcon, "aria-hidden": "true", children: [
11622
- /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "6", y: "5", width: "4", height: "14", rx: "1.2" }),
11623
- /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "14", y: "5", width: "4", height: "14", rx: "1.2" })
11624
- ] }) : /* @__PURE__ */ jsxRuntime.jsx(
11625
- "svg",
11626
- {
11627
- viewBox: "0 0 24 24",
11628
- style: { ...styles.playIcon, ...styles.playIconOffset },
11629
- "aria-hidden": "true",
11630
- children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M7 5.2c0-1.02 1.12-1.65 2-.98l10.2 7.78a1.24 1.24 0 0 1 0 1.98L9 21.76c-.88.67-2-.04-2-1.08V5.2Z" })
11631
- }
11632
- )
11633
- }
11634
- ),
11635
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.content, children: [
11636
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.metaRow, children: [
11637
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: formatTime(currentTime / speed) }),
11638
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.metaRight, children: [
11639
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.badge, children: narrationLabel }),
11640
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: formatTime(duration / speed) })
11641
- ] })
11642
- ] }),
11643
- /* @__PURE__ */ jsxRuntime.jsx(
11644
- "div",
11645
- {
11646
- ref: scrubRef,
11647
- role: "slider",
11648
- "aria-label": "Narration progress",
11649
- "aria-valuemin": 0,
11650
- "aria-valuemax": Math.max(1, Math.floor(duration)),
11651
- "aria-valuenow": Math.floor(currentTime),
11652
- tabIndex: 0,
11653
- onClick: (event) => {
11654
- seekTo(event.clientX);
11655
- },
11656
- style: styles.scrubber,
11657
- children: peaks.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
11658
- "svg",
11659
- {
11660
- viewBox: `0 0 ${peaks.length} 40`,
11661
- preserveAspectRatio: "none",
11662
- style: styles.waveform,
11663
- children: peaks.map((peak, index) => {
11664
- const height = Math.max(2, Math.round(Math.abs(peak) * 34));
11665
- const y = (40 - height) / 2;
11666
- const filled = index / peaks.length < progress;
11667
- return /* @__PURE__ */ jsxRuntime.jsx(
11668
- "rect",
11669
- {
11670
- x: index,
11671
- y,
11672
- width: 0.82,
11673
- height,
11674
- rx: 0.4,
11675
- fill: filled ? "#155dfc" : "#90c5ff"
11676
- },
11677
- `${audio.id}-${index}`
11678
- );
11679
- })
11680
- }
11681
- ) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
11682
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.fallbackTrack }),
11683
- /* @__PURE__ */ jsxRuntime.jsx(
11684
- "div",
11685
- {
11686
- style: { ...styles.fallbackProgress, width: `${progress * 100}%` }
11687
- }
11688
- )
11689
- ] })
11690
- }
11691
- ),
11692
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.chipsRow, children: [
11693
- /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles.chipStrong, children: [
11694
- "Provider: ",
11695
- providerLabel
11696
- ] }),
11697
- duration > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles.chipMuted, children: [
11698
- "Length ",
11699
- formatTime(duration / speed)
11700
- ] }),
11701
- expiresLabel && /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles.chipMuted, children: [
11702
- "Expires ",
11703
- expiresLabel
11704
- ] })
11705
- ] })
11706
- ] }),
11707
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.controls, children: [
11708
- /* @__PURE__ */ jsxRuntime.jsxs(
11709
- "button",
11710
- {
11711
- type: "button",
11712
- onClick: () => {
11713
- setSpeed((currentSpeed) => getNextSpeed(currentSpeed));
11714
- },
11715
- style: styles.secondaryButton,
11716
- children: [
11717
- speed,
11718
- "\xD7"
11719
- ]
11720
- }
11721
- ),
11722
- onRefresh && showRefreshButton && /* @__PURE__ */ jsxRuntime.jsx(
11723
- "button",
11724
- {
11725
- type: "button",
11726
- onClick: onRefresh,
11727
- disabled: refreshing,
11728
- style: refreshing ? { ...styles.refreshButton, ...styles.disabled } : styles.refreshButton,
11729
- children: refreshing ? "Refreshing\u2026" : "Refresh"
11730
- }
11731
- )
11732
- ] })
11733
- ] })
11618
+ );
11619
+ const downBtn = /* @__PURE__ */ jsxRuntime.jsx(
11620
+ "button",
11621
+ {
11622
+ type: "button",
11623
+ "aria-label": downLabel,
11624
+ "aria-pressed": downActive,
11625
+ title: isAuthenticated ? downLabel : "Sign in to vote",
11626
+ className: [classNames?.downButton, downActive ? classNames?.downButtonActive : void 0].filter(Boolean).join(" ") || void 0,
11627
+ onClick: () => void handleVote(-1),
11628
+ style: classNames?.downButton ? void 0 : { ...STYLES2.button, ...compactStyle, ...downActive ? STYLES2.buttonActiveDown : {} },
11629
+ children: ARROW_DOWN
11630
+ }
11631
+ );
11632
+ const scoreClass = [
11633
+ classNames?.score,
11634
+ state.score > 0 ? classNames?.scorePositive : void 0,
11635
+ state.score < 0 ? classNames?.scoreNegative : void 0
11636
+ ].filter(Boolean).join(" ") || void 0;
11637
+ const scoreEl = /* @__PURE__ */ jsxRuntime.jsx("span", { className: scoreClass, style: classNames?.score ? void 0 : STYLES2.score, children: state.score });
11638
+ const rootStyle = layout === "stacked" ? STYLES2.rootStacked : STYLES2.root;
11639
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classNames?.root, style: classNames?.root ? void 0 : rootStyle, children: [
11640
+ upBtn,
11641
+ scoreEl,
11642
+ downBtn
11734
11643
  ] });
11735
11644
  }
11736
11645
 
@@ -12034,11 +11943,11 @@ Object.defineProperty(exports, "createMoneyClient", {
12034
11943
  get: function () { return money.createMoneyClient; }
12035
11944
  });
12036
11945
  exports.FeedbackWidget = FeedbackWidget;
12037
- exports.NarrationPlayer = NarrationPlayer;
12038
11946
  exports.ScaleMuleApiError = ScaleMuleApiError;
12039
11947
  exports.ScaleMuleClient = ScaleMuleClient2;
12040
11948
  exports.ScaleMuleMedia = ScaleMuleMedia;
12041
11949
  exports.ScaleMuleProvider = ScaleMuleProvider;
11950
+ exports.VoteButton = VoteButton;
12042
11951
  exports.composePhone = composePhone;
12043
11952
  exports.createClient = createClient;
12044
11953
  exports.createSafeLogger = createSafeLogger;
@@ -12063,5 +11972,6 @@ exports.useScaleMuleClient = useScaleMuleClient;
12063
11972
  exports.useShare = useShare;
12064
11973
  exports.useTtsJob = useTtsJob;
12065
11974
  exports.useUser = useUser;
11975
+ exports.useVote = useVote;
12066
11976
  exports.validateForm = validateForm;
12067
11977
  exports.validators = validators;