@industry-theme/file-city-panel 0.2.80 → 0.3.1

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.
@@ -50650,13 +50650,22 @@ const TourPlayer = ({
50650
50650
  onInteractiveAction,
50651
50651
  visible = true,
50652
50652
  position,
50653
- skipWelcome = false
50653
+ skipWelcome = false,
50654
+ ttsAdapter,
50655
+ tourAudioContext,
50656
+ autoPlayAudio = false,
50657
+ autoAdvanceOnAudioEnd = false,
50658
+ autoAdvanceDelay = 1e3
50654
50659
  }) => {
50655
50660
  const { theme: theme2 } = useTheme();
50656
50661
  const [internalStepIndex, setInternalStepIndex] = useState(0);
50657
50662
  const [completedSteps, setCompletedSteps] = useState(/* @__PURE__ */ new Set());
50658
50663
  const [isPlaying, setIsPlaying] = useState(skipWelcome);
50659
50664
  const [autoAdvanceTimeRemaining, setAutoAdvanceTimeRemaining] = useState(null);
50665
+ const [ttsState, setTtsState] = useState((ttsAdapter == null ? void 0 : ttsAdapter.state) || null);
50666
+ const [audioReady, setAudioReady] = useState(false);
50667
+ const [isAutoPlaying, setIsAutoPlaying] = useState(false);
50668
+ const ttsStateUpdateInterval = useRef(null);
50660
50669
  const currentStepIndex = controlledStepIndex !== void 0 ? controlledStepIndex : internalStepIndex;
50661
50670
  const currentStep = tour.steps[currentStepIndex];
50662
50671
  const isFirstStep = currentStepIndex === 0;
@@ -50722,6 +50731,100 @@ const TourPlayer = ({
50722
50731
  },
50723
50732
  [onInteractiveAction]
50724
50733
  );
50734
+ useEffect(() => {
50735
+ if (!ttsAdapter || !tourAudioContext) return;
50736
+ async function loadAudio() {
50737
+ try {
50738
+ const stepIds = tour.steps.map((step) => step.id);
50739
+ await ttsAdapter.fetchTourAudio(tour.id, stepIds, tourAudioContext);
50740
+ setAudioReady(true);
50741
+ } catch (error) {
50742
+ console.error("[TourPlayer] Failed to load tour audio:", error);
50743
+ }
50744
+ }
50745
+ loadAudio();
50746
+ }, [ttsAdapter, tourAudioContext, tour]);
50747
+ useEffect(() => {
50748
+ if (!ttsAdapter) return;
50749
+ ttsStateUpdateInterval.current = setInterval(() => {
50750
+ setTtsState({ ...ttsAdapter.state });
50751
+ }, 100);
50752
+ return () => {
50753
+ if (ttsStateUpdateInterval.current) {
50754
+ clearInterval(ttsStateUpdateInterval.current);
50755
+ ttsStateUpdateInterval.current = null;
50756
+ }
50757
+ };
50758
+ }, [ttsAdapter]);
50759
+ useEffect(() => {
50760
+ if (!ttsAdapter || !autoAdvanceOnAudioEnd || !isAutoPlaying) return;
50761
+ const handleAudioEnded = () => {
50762
+ console.log("[TourPlayer] Audio ended, auto-advancing...");
50763
+ setTimeout(() => {
50764
+ if (!isLastStep) {
50765
+ nextStep();
50766
+ } else {
50767
+ console.log("[TourPlayer] Tour complete");
50768
+ setIsAutoPlaying(false);
50769
+ handleExit();
50770
+ }
50771
+ }, autoAdvanceDelay);
50772
+ };
50773
+ ttsAdapter.addEventListener("ended", handleAudioEnded);
50774
+ return () => ttsAdapter.removeEventListener("ended", handleAudioEnded);
50775
+ }, [ttsAdapter, autoAdvanceOnAudioEnd, isAutoPlaying, isLastStep, autoAdvanceDelay, nextStep, handleExit]);
50776
+ useEffect(() => {
50777
+ if (!ttsAdapter || !autoPlayAudio || !isPlaying || !audioReady || !isAutoPlaying) return;
50778
+ const playCurrentStep = async () => {
50779
+ try {
50780
+ await ttsAdapter.speak(currentStep.id);
50781
+ } catch (error) {
50782
+ console.error("[TourPlayer] Auto-play failed:", error);
50783
+ setIsAutoPlaying(false);
50784
+ }
50785
+ };
50786
+ playCurrentStep();
50787
+ }, [ttsAdapter, autoPlayAudio, currentStep == null ? void 0 : currentStep.id, isPlaying, audioReady, isAutoPlaying]);
50788
+ useEffect(() => {
50789
+ return () => {
50790
+ if (ttsAdapter) {
50791
+ ttsAdapter.stop();
50792
+ }
50793
+ };
50794
+ }, [ttsAdapter]);
50795
+ const handleReadAloud = useCallback(async () => {
50796
+ if (!ttsAdapter || !audioReady) return;
50797
+ if (ttsState == null ? void 0 : ttsState.isPlaying) {
50798
+ ttsAdapter.stop();
50799
+ return;
50800
+ }
50801
+ if (ttsState == null ? void 0 : ttsState.isPaused) {
50802
+ ttsAdapter.resume();
50803
+ return;
50804
+ }
50805
+ try {
50806
+ await ttsAdapter.speak(currentStep.id);
50807
+ } catch (error) {
50808
+ console.error("[TourPlayer] TTS failed:", error);
50809
+ }
50810
+ }, [ttsAdapter, ttsState, audioReady, currentStep == null ? void 0 : currentStep.id]);
50811
+ const toggleAutoPlay = useCallback(() => {
50812
+ if (!ttsAdapter || !audioReady) return;
50813
+ if (isAutoPlaying) {
50814
+ setIsAutoPlaying(false);
50815
+ ttsAdapter.stop();
50816
+ } else {
50817
+ setIsAutoPlaying(true);
50818
+ ttsAdapter.speak(currentStep.id);
50819
+ }
50820
+ }, [isAutoPlaying, ttsAdapter, audioReady, currentStep == null ? void 0 : currentStep.id]);
50821
+ const formatTime = (seconds) => {
50822
+ const mins = Math.floor(seconds / 60);
50823
+ const secs = Math.floor(seconds % 60);
50824
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
50825
+ };
50826
+ const showTTSControls = !!ttsAdapter && audioReady;
50827
+ const canAutoPlay = autoPlayAudio && autoAdvanceOnAudioEnd;
50725
50828
  if (!visible) return null;
50726
50829
  const useAbsolutePositioning = position === "top" || position === "overlay";
50727
50830
  const absoluteContainerStyle = {
@@ -50980,6 +51083,40 @@ const TourPlayer = ({
50980
51083
  "Start Tour"
50981
51084
  ]
50982
51085
  }
51086
+ ),
51087
+ showTTSControls && canAutoPlay && /* @__PURE__ */ jsxs(
51088
+ "button",
51089
+ {
51090
+ onClick: () => {
51091
+ handleStart();
51092
+ setIsAutoPlaying(true);
51093
+ },
51094
+ style: {
51095
+ display: "flex",
51096
+ alignItems: "center",
51097
+ gap: "6px",
51098
+ padding: "8px 16px",
51099
+ backgroundColor: theme2.colors.accent,
51100
+ color: "#ffffff",
51101
+ border: "none",
51102
+ borderRadius: "6px",
51103
+ fontSize: theme2.fontSizes[1],
51104
+ fontFamily: theme2.fonts.body,
51105
+ fontWeight: 600,
51106
+ cursor: "pointer",
51107
+ transition: "all 0.2s"
51108
+ },
51109
+ onMouseEnter: (e) => {
51110
+ e.currentTarget.style.backgroundColor = theme2.colors.accent + "dd";
51111
+ },
51112
+ onMouseLeave: (e) => {
51113
+ e.currentTarget.style.backgroundColor = theme2.colors.accent;
51114
+ },
51115
+ children: [
51116
+ /* @__PURE__ */ jsx(Volume2, { size: 16 }),
51117
+ "Start with Auto-Play"
51118
+ ]
51119
+ }
50983
51120
  )
50984
51121
  ]
50985
51122
  }
@@ -51276,6 +51413,121 @@ const TourPlayer = ({
51276
51413
  ] })
51277
51414
  }
51278
51415
  ),
51416
+ showTTSControls && /* @__PURE__ */ jsxs(
51417
+ "div",
51418
+ {
51419
+ style: {
51420
+ padding: "12px 16px",
51421
+ borderTop: `1px solid ${theme2.colors.border}`,
51422
+ flexShrink: 0,
51423
+ display: "flex",
51424
+ flexDirection: "column",
51425
+ gap: "8px"
51426
+ },
51427
+ children: [
51428
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
51429
+ /* @__PURE__ */ jsxs(
51430
+ "button",
51431
+ {
51432
+ onClick: handleReadAloud,
51433
+ disabled: (ttsState == null ? void 0 : ttsState.isLoading) || !audioReady,
51434
+ style: {
51435
+ display: "flex",
51436
+ alignItems: "center",
51437
+ gap: "6px",
51438
+ padding: "8px 12px",
51439
+ backgroundColor: "transparent",
51440
+ color: theme2.colors.text,
51441
+ border: `1px solid ${theme2.colors.border}`,
51442
+ borderRadius: "8px",
51443
+ fontSize: theme2.fontSizes[1],
51444
+ fontFamily: theme2.fonts.body,
51445
+ cursor: (ttsState == null ? void 0 : ttsState.isLoading) || !audioReady ? "not-allowed" : "pointer",
51446
+ opacity: (ttsState == null ? void 0 : ttsState.isLoading) || !audioReady ? 0.5 : 1,
51447
+ transition: "all 0.2s"
51448
+ },
51449
+ onMouseEnter: (e) => {
51450
+ if (!(ttsState == null ? void 0 : ttsState.isLoading) && audioReady) {
51451
+ e.currentTarget.style.backgroundColor = theme2.colors.backgroundLight;
51452
+ }
51453
+ },
51454
+ onMouseLeave: (e) => {
51455
+ e.currentTarget.style.backgroundColor = "transparent";
51456
+ },
51457
+ children: [
51458
+ (ttsState == null ? void 0 : ttsState.isLoading) && /* @__PURE__ */ jsx(LoaderCircle, { size: 16, className: "animate-spin", style: { animation: "spin 1s linear infinite" } }),
51459
+ (ttsState == null ? void 0 : ttsState.isPlaying) && /* @__PURE__ */ jsx(Volume2, { size: 16 }),
51460
+ (ttsState == null ? void 0 : ttsState.isPaused) && /* @__PURE__ */ jsx(VolumeX, { size: 16 }),
51461
+ !(ttsState == null ? void 0 : ttsState.isPlaying) && !(ttsState == null ? void 0 : ttsState.isPaused) && !(ttsState == null ? void 0 : ttsState.isLoading) && /* @__PURE__ */ jsx(Volume2, { size: 16 }),
51462
+ /* @__PURE__ */ jsx("span", { children: (ttsState == null ? void 0 : ttsState.isLoading) ? "Loading..." : (ttsState == null ? void 0 : ttsState.isPlaying) ? "Stop" : (ttsState == null ? void 0 : ttsState.isPaused) ? "Resume" : "Read Aloud" })
51463
+ ]
51464
+ }
51465
+ ),
51466
+ canAutoPlay && /* @__PURE__ */ jsxs(
51467
+ "button",
51468
+ {
51469
+ onClick: toggleAutoPlay,
51470
+ disabled: !audioReady,
51471
+ style: {
51472
+ display: "flex",
51473
+ alignItems: "center",
51474
+ gap: "6px",
51475
+ padding: "8px 12px",
51476
+ backgroundColor: isAutoPlaying ? theme2.colors.primary : "transparent",
51477
+ color: isAutoPlaying ? "#ffffff" : theme2.colors.text,
51478
+ border: `1px solid ${isAutoPlaying ? theme2.colors.primary : theme2.colors.border}`,
51479
+ borderRadius: "8px",
51480
+ fontSize: theme2.fontSizes[1],
51481
+ fontFamily: theme2.fonts.body,
51482
+ cursor: !audioReady ? "not-allowed" : "pointer",
51483
+ opacity: !audioReady ? 0.5 : 1,
51484
+ transition: "all 0.2s"
51485
+ },
51486
+ onMouseEnter: (e) => {
51487
+ if (audioReady) {
51488
+ e.currentTarget.style.opacity = "0.8";
51489
+ }
51490
+ },
51491
+ onMouseLeave: (e) => {
51492
+ e.currentTarget.style.opacity = "1";
51493
+ },
51494
+ children: [
51495
+ isAutoPlaying ? /* @__PURE__ */ jsx(Pause, { size: 16 }) : /* @__PURE__ */ jsx(Play, { size: 16 }),
51496
+ /* @__PURE__ */ jsx("span", { children: isAutoPlaying ? "Stop Auto-Play" : "Start Auto-Play" })
51497
+ ]
51498
+ }
51499
+ )
51500
+ ] }),
51501
+ (ttsState == null ? void 0 : ttsState.isPlaying) && ttsState.duration && /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
51502
+ /* @__PURE__ */ jsx("span", { style: { fontSize: theme2.fontSizes[0], color: theme2.colors.textSecondary, minWidth: "35px" }, children: formatTime(ttsState.currentTime || 0) }),
51503
+ /* @__PURE__ */ jsx(
51504
+ "div",
51505
+ {
51506
+ style: {
51507
+ flex: 1,
51508
+ height: "4px",
51509
+ backgroundColor: theme2.colors.backgroundLight,
51510
+ borderRadius: "2px",
51511
+ overflow: "hidden"
51512
+ },
51513
+ children: /* @__PURE__ */ jsx(
51514
+ "div",
51515
+ {
51516
+ style: {
51517
+ height: "100%",
51518
+ width: `${ttsState.progress || 0}%`,
51519
+ backgroundColor: theme2.colors.primary,
51520
+ transition: "width 0.1s linear"
51521
+ }
51522
+ }
51523
+ )
51524
+ }
51525
+ ),
51526
+ /* @__PURE__ */ jsx("span", { style: { fontSize: theme2.fontSizes[0], color: theme2.colors.textSecondary, minWidth: "35px" }, children: formatTime(ttsState.duration) })
51527
+ ] })
51528
+ ]
51529
+ }
51530
+ ),
51279
51531
  /* @__PURE__ */ jsxs(
51280
51532
  "div",
51281
51533
  {
@@ -56068,6 +56320,212 @@ const ProjectInfoHeader = ({
56068
56320
  }
56069
56321
  );
56070
56322
  };
56323
+ class MockTTSAdapter {
56324
+ constructor(config = {}) {
56325
+ this.audioCache = /* @__PURE__ */ new Map();
56326
+ this.eventTarget = new EventTarget();
56327
+ this.playbackTimer = null;
56328
+ this.progressTimer = null;
56329
+ this._state = {
56330
+ isLoading: false,
56331
+ isPlaying: false,
56332
+ isPaused: false,
56333
+ error: null
56334
+ };
56335
+ this.config = {
56336
+ audioDuration: config.audioDuration ?? 5,
56337
+ playbackDelay: config.playbackDelay ?? 100,
56338
+ fetchDelay: config.fetchDelay ?? 500,
56339
+ verbose: config.verbose ?? true,
56340
+ simulateError: config.simulateError ?? false
56341
+ };
56342
+ }
56343
+ get state() {
56344
+ return { ...this._state };
56345
+ }
56346
+ setState(updates) {
56347
+ this._state = { ...this._state, ...updates };
56348
+ }
56349
+ log(...args) {
56350
+ if (this.config.verbose) {
56351
+ console.log("[Mock TTS]", ...args);
56352
+ }
56353
+ }
56354
+ /**
56355
+ * Fetch audio URLs for all steps in a tour
56356
+ */
56357
+ async fetchTourAudio(tourId, stepIds, context, options) {
56358
+ this.log("Fetching tour audio:", { tourId, stepCount: stepIds.length, context, options });
56359
+ this.setState({ isLoading: true, error: null });
56360
+ this.emit("loading");
56361
+ await new Promise((resolve) => setTimeout(resolve, this.config.fetchDelay));
56362
+ if (this.config.simulateError) {
56363
+ const error = new Error("Mock TTS: Simulated fetch error");
56364
+ this.setState({ isLoading: false, error });
56365
+ this.emit("error");
56366
+ throw error;
56367
+ }
56368
+ this.audioCache.clear();
56369
+ for (const stepId of stepIds) {
56370
+ const mockUrl = `mock://audio/${tourId}/${stepId}.mp3`;
56371
+ this.audioCache.set(stepId, mockUrl);
56372
+ }
56373
+ this.setState({ isLoading: false });
56374
+ this.log(`Loaded ${this.audioCache.size} audio files`);
56375
+ return new Map(this.audioCache);
56376
+ }
56377
+ /**
56378
+ * Play audio for a specific step
56379
+ */
56380
+ async speak(stepId) {
56381
+ const audioUrl = this.audioCache.get(stepId);
56382
+ if (!audioUrl) {
56383
+ const error = new Error(`Mock TTS: No audio URL for step: ${stepId}`);
56384
+ this.setState({ error });
56385
+ this.emit("error");
56386
+ throw error;
56387
+ }
56388
+ this.log("Speaking step:", stepId);
56389
+ this.stop();
56390
+ if (this.config.simulateError) {
56391
+ const error = new Error("Mock TTS: Simulated playback error");
56392
+ this.setState({ error });
56393
+ this.emit("error");
56394
+ throw error;
56395
+ }
56396
+ await new Promise((resolve) => setTimeout(resolve, this.config.playbackDelay));
56397
+ this.setState({
56398
+ isPlaying: true,
56399
+ isPaused: false,
56400
+ error: null,
56401
+ duration: this.config.audioDuration,
56402
+ currentTime: 0,
56403
+ progress: 0
56404
+ });
56405
+ this.emit("playing");
56406
+ const startTime = Date.now();
56407
+ this.progressTimer = setInterval(() => {
56408
+ const elapsed = (Date.now() - startTime) / 1e3;
56409
+ const progress = Math.min(elapsed / this.config.audioDuration * 100, 100);
56410
+ this.setState({
56411
+ currentTime: elapsed,
56412
+ progress
56413
+ });
56414
+ }, 100);
56415
+ this.playbackTimer = setTimeout(() => {
56416
+ this.log("Playback ended for step:", stepId);
56417
+ this.cleanupPlayback();
56418
+ this.setState({
56419
+ isPlaying: false,
56420
+ isPaused: false,
56421
+ currentTime: 0,
56422
+ progress: 0
56423
+ });
56424
+ this.emit("ended");
56425
+ }, this.config.audioDuration * 1e3);
56426
+ }
56427
+ /**
56428
+ * Stop current audio playback
56429
+ */
56430
+ stop() {
56431
+ this.log("Stopping playback");
56432
+ this.cleanupPlayback();
56433
+ this.setState({
56434
+ isPlaying: false,
56435
+ isPaused: false,
56436
+ currentTime: 0,
56437
+ progress: 0
56438
+ });
56439
+ }
56440
+ /**
56441
+ * Pause current audio playback
56442
+ */
56443
+ pause() {
56444
+ if (this._state.isPlaying) {
56445
+ this.log("Pausing playback");
56446
+ this.cleanupPlayback();
56447
+ this.setState({ isPlaying: false, isPaused: true });
56448
+ this.emit("paused");
56449
+ }
56450
+ }
56451
+ /**
56452
+ * Resume paused audio playback
56453
+ */
56454
+ resume() {
56455
+ if (this._state.isPaused && this._state.duration) {
56456
+ this.log("Resuming playback");
56457
+ const remainingTime = this._state.duration - (this._state.currentTime || 0);
56458
+ const startTime = Date.now() - (this._state.currentTime || 0) * 1e3;
56459
+ this.setState({ isPlaying: true, isPaused: false });
56460
+ this.emit("playing");
56461
+ this.progressTimer = setInterval(() => {
56462
+ const elapsed = (Date.now() - startTime) / 1e3;
56463
+ const progress = Math.min(elapsed / this._state.duration * 100, 100);
56464
+ this.setState({
56465
+ currentTime: elapsed,
56466
+ progress
56467
+ });
56468
+ }, 100);
56469
+ this.playbackTimer = setTimeout(() => {
56470
+ this.log("Playback ended (resumed)");
56471
+ this.cleanupPlayback();
56472
+ this.setState({
56473
+ isPlaying: false,
56474
+ isPaused: false,
56475
+ currentTime: 0,
56476
+ progress: 0
56477
+ });
56478
+ this.emit("ended");
56479
+ }, remainingTime * 1e3);
56480
+ }
56481
+ }
56482
+ /**
56483
+ * Subscribe to TTS events
56484
+ */
56485
+ addEventListener(event, handler) {
56486
+ this.eventTarget.addEventListener(event, handler);
56487
+ }
56488
+ /**
56489
+ * Unsubscribe from TTS events
56490
+ */
56491
+ removeEventListener(event, handler) {
56492
+ this.eventTarget.removeEventListener(event, handler);
56493
+ }
56494
+ /**
56495
+ * Emit an event
56496
+ */
56497
+ emit(event) {
56498
+ this.eventTarget.dispatchEvent(new Event(event));
56499
+ }
56500
+ /**
56501
+ * Clean up timers
56502
+ */
56503
+ cleanupPlayback() {
56504
+ if (this.playbackTimer) {
56505
+ clearTimeout(this.playbackTimer);
56506
+ this.playbackTimer = null;
56507
+ }
56508
+ if (this.progressTimer) {
56509
+ clearInterval(this.progressTimer);
56510
+ this.progressTimer = null;
56511
+ }
56512
+ }
56513
+ /**
56514
+ * Clear all cached audio URLs
56515
+ */
56516
+ clearCache() {
56517
+ this.audioCache.clear();
56518
+ }
56519
+ /**
56520
+ * Get cache statistics (for debugging)
56521
+ */
56522
+ getCacheStats() {
56523
+ return {
56524
+ size: this.audioCache.size,
56525
+ entries: Array.from(this.audioCache.keys())
56526
+ };
56527
+ }
56528
+ }
56071
56529
  const panels = [
56072
56530
  {
56073
56531
  metadata: {
@@ -56145,10 +56603,12 @@ export {
56145
56603
  FeedProjectHeaderSkeleton,
56146
56604
  FileCardList,
56147
56605
  GitChangesCardList,
56606
+ MockTTSAdapter,
56148
56607
  PrChangesCardList,
56149
56608
  ProjectInfoHeader,
56150
56609
  QUALITY_COLOR_MODES,
56151
56610
  StoryboardFilesCardList,
56611
+ TourPlayer,
56152
56612
  codeCityPanelTools,
56153
56613
  codeCityPanelToolsMetadata,
56154
56614
  focusBuildingTool,