Skip to content

Restore scene rotator, tablet blog/audio tabs, and ambient audio synth#87

Open
alexwelcing wants to merge 1 commit intomainfrom
codex/prioritize-outstanding-development-tasks
Open

Restore scene rotator, tablet blog/audio tabs, and ambient audio synth#87
alexwelcing wants to merge 1 commit intomainfrom
codex/prioritize-outstanding-development-tasks

Conversation

@alexwelcing
Copy link
Copy Markdown
Owner

Motivation

  • Restore the in-scene scenery rotation so users can cycle between panorama images and newly-detected Gaussian splats from the tablet UI.
  • Surface blog/article and ambient-audio controls inside the pip‑boy tablet so AI chat, articles, and audio are reachable without leaving the 3D scene.
  • Avoid committing binary audio assets while still providing immersive ambient sound, and persist user audio prefs across sessions.
  • Provide a backend API response that lists available panorama images so the UI can present a full rotator of scenery options.

Description

  • Added a components/AmbientAudio.tsx Web Audio-based synth and replaced the committed WAV file with a synthesized loop, and persisted ambientAudioEnabled and ambientAudioVolume in localStorage.
  • Extended pages/api/backgroundImages.ts to return an image list when called with ?mode=list and updated components/ThreeSixty.tsx to fetch and expose availableImages and build sceneryOptions including splats and panoramas.
  • Implemented scene rotation controls (handleRotateScenery) and wired onRotateScenery through InteractiveTablet.tsxTerminalInterface.tsx, adding prev/next buttons and click-to-select scenery tiles.
  • Expanded the tablet/terminal UI with BLOG and AUDIO tabs, article "READ" actions that update journey stats, and audio controls (mute/volume/status) integrated into the tablet.

Testing

  • Started the dev server (npm run dev) and confirmed the app compiled and served pages successfully (dev server reported Ready).
  • Performed an HTTP smoke test using curl which returned 200 OK for the home page.
  • Attempted an automated Playwright scenario to open the 3D view and capture a terminal screenshot, but the Playwright run failed to connect in this environment (connection refused / timeout).
  • No unit or integration test suites were run as part of this change set (none requested).

Codex Task

@vercel
Copy link
Copy Markdown

vercel Bot commented Dec 20, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
next-docs-search Ready Ready Preview, Comment Dec 20, 2025 10:58pm

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR restores scene rotation functionality, adds blog/audio tabs to the tablet interface, and implements an ambient audio synthesizer to replace binary audio files. The changes enable users to cycle through panorama images and Gaussian splats, access articles and audio controls from the tablet UI, and persist audio preferences across sessions.

Key Changes:

  • Implemented Web Audio API-based ambient audio with localStorage persistence for audio preferences
  • Extended the background images API to support listing all available panoramas
  • Added scene rotation controls with prev/next navigation and click-to-select tiles in the tablet UI
  • Integrated BLOG and AUDIO tabs into the tablet terminal interface with article reading and audio controls

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
components/AmbientAudio.tsx New component implementing Web Audio synthesizer for ambient sound with browser autoplay handling
pages/api/backgroundImages.ts Extended API to support list mode returning all available background images
components/ThreeSixty.tsx Added audio state management, background image fetching, and scene rotation handler
components/TerminalInterface.tsx Added BLOG and AUDIO tabs with article display, read tracking, and audio controls
components/InteractiveTablet.tsx Updated tablet menu to support new tabs and pass audio/rotation props to terminal
pages/index.tsx Modified image change handler to accept specific image paths or fallback to random
pages/chat.tsx Updated image change handler signature to match new pattern

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}

res.status(200).json({
image: `/background/${randomImage}`,
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API now returns image paths with a leading slash (/background/${file}), but previously returned paths with a dot-slash prefix (./background/${randomImage}). This change in path format could break existing code that expects the old format. Ensure all consumers of this API are updated to handle the new path format correctly.

Copilot uses AI. Check for mistakes.
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gridTemplateColumns: isMobile ? 'repeat(2, 1fr)' : 'repeat(3, 1fr)',
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The grid layout changes from a fixed 2-column layout to a conditional 2 or 3-column layout based on isMobile. However, with 6 action buttons (ASK AI, BLOG, GAME, SCENE, AUDIO, ABOUT), a 2-column grid on mobile will create 3 rows, while a 3-column grid on desktop creates 2 rows. Consider if this is the intended layout behavior, as having 6 items evenly divisible by both 2 and 3 might benefit from a different responsive approach.

Suggested change
gridTemplateColumns: isMobile ? 'repeat(2, 1fr)' : 'repeat(3, 1fr)',
gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))',

Copilot uses AI. Check for mistakes.
Comment on lines +90 to +92
if (!activeView || !isOpen) return;
setViewMode(activeView);
}, [activeView, isOpen]);
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The activeView prop is passed to TerminalInterface and used to set viewMode via useEffect, but this creates a potential sync issue. If activeView changes while the terminal is open, it will switch views. However, if activeView changes while the terminal is closed, the effect won't run (due to the isOpen check), and when the terminal opens again, it might show a stale view. Consider removing the isOpen check from the dependency array or restructuring this logic.

Suggested change
if (!activeView || !isOpen) return;
setViewMode(activeView);
}, [activeView, isOpen]);
if (!activeView) return;
setViewMode(activeView);
}, [activeView]);

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +59
const attemptPlay = useCallback(async () => {
try {
if (!contextRef.current) {
contextRef.current = new AudioContext();
}
const context = contextRef.current;
await context.resume();

if (!gainRef.current) {
gainRef.current = context.createGain();
gainRef.current.gain.value = volume;
gainRef.current.connect(context.destination);
}

if (!oscillatorRef.current) {
const oscillator = context.createOscillator();
oscillator.type = 'sine';
oscillator.frequency.value = 220;
oscillator.connect(gainRef.current);
oscillator.start();
oscillatorRef.current = oscillator;
}

setBlocked(false);
onStatusChange?.('playing');
} catch (error) {
setBlocked(true);
onStatusChange?.('blocked');
}
}, [onStatusChange, volume]);
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The attemptPlay callback has volume in its dependency array, but the volume is set once when gainRef.current is created. When volume changes and attemptPlay is recreated, it won't update the gain value for an existing oscillator. The volume update is handled in a separate useEffect (lines 61-65), but this means attemptPlay will be recreated on every volume change even though it doesn't need the latest volume value. Consider removing volume from the dependency array since the gain update is handled separately.

Copilot uses AI. Check for mistakes.
Comment thread pages/index.tsx
Comment on lines +65 to +71
function handleImageChange(newImage: string) {
if (newImage) {
setCurrentImage(newImage)
return
}
getRandomImage()
}
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handleImageChange function accepts a newImage parameter but the function signature doesn't validate or document what happens when an empty string is passed. If newImage is an empty string, the condition if (newImage) will be false and getRandomImage() will be called. Consider being more explicit about this behavior or adding validation for valid image paths.

Copilot uses AI. Check for mistakes.
Comment thread components/ThreeSixty.tsx
Comment on lines +394 to +403
const fetchBackgrounds = async () => {
try {
const response = await fetch('/api/backgroundImages?mode=list');
if (!response.ok) return;
const data = await response.json();
if (Array.isArray(data.images)) {
setAvailableImages(data.images);
}
} catch (error) {
console.error('Failed fetching background images:', error);
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the API request fails or returns a non-ok response, the error is silently logged but availableImages remains as an empty array. This means users won't see panorama options if the API fails. Consider adding error handling UI or retry logic to improve the user experience when the background images API is unavailable.

Suggested change
const fetchBackgrounds = async () => {
try {
const response = await fetch('/api/backgroundImages?mode=list');
if (!response.ok) return;
const data = await response.json();
if (Array.isArray(data.images)) {
setAvailableImages(data.images);
}
} catch (error) {
console.error('Failed fetching background images:', error);
const fetchBackgrounds = async (attempt: number = 1) => {
try {
const response = await fetch('/api/backgroundImages?mode=list');
if (!response.ok) {
throw new Error(`Background images request failed with status ${response.status}`);
}
const data = await response.json();
if (Array.isArray(data.images)) {
setAvailableImages(data.images);
}
} catch (error) {
console.error(`Failed fetching background images (attempt ${attempt}):`, error);
const maxAttempts = 3;
if (attempt < maxAttempts) {
const retryDelayMs = 1000 * attempt;
setTimeout(() => {
fetchBackgrounds(attempt + 1);
}, retryDelayMs);
}

Copilot uses AI. Check for mistakes.
Comment on lines +164 to +168
updateStats('articlesRead', [slug]);
if (currentQuest?.id === 'read-article') {
completeQuest('read-article');
}
window.open(`/articles/${slug}`, '_blank');
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The article filename is used as a fallback for the slug, but filenames may contain characters that are not URL-safe. Consider sanitizing the filename or ensuring it's properly encoded before using it in the URL path. Additionally, if the article has no filename and the title contains special characters, spaces, or slashes, this could result in a malformed URL.

Suggested change
updateStats('articlesRead', [slug]);
if (currentQuest?.id === 'read-article') {
completeQuest('read-article');
}
window.open(`/articles/${slug}`, '_blank');
const safeSlug = encodeURIComponent(slug);
updateStats('articlesRead', [slug]);
if (currentQuest?.id === 'read-article') {
completeQuest('read-article');
}
window.open(`/articles/${safeSlug}`, '_blank');

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +51
if (!oscillatorRef.current) {
const oscillator = context.createOscillator();
oscillator.type = 'sine';
oscillator.frequency.value = 220;
oscillator.connect(gainRef.current);
oscillator.start();
oscillatorRef.current = oscillator;
}
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The oscillator is created and started but never stopped if attemptPlay is called multiple times while an oscillator is already running. This can lead to multiple oscillators playing simultaneously. Consider checking if an oscillator already exists and is connected before creating a new one, or stop the existing oscillator before creating a new one.

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +73
useEffect(() => {
if (!enabled) {
stopAudio();
return;
}
attemptPlay();
}, [attemptPlay, enabled, stopAudio]);
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stopAudio and attemptPlay callbacks are included in the dependency array of the useEffect that manages audio playback (line 73). This creates a circular dependency: when stopAudio changes, the effect runs, which may call attemptPlay, causing the effect to re-run. This can lead to unnecessary re-execution. Consider using refs for these functions or restructure the logic to avoid this circular dependency.

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +96
useEffect(() => {
return () => {
stopAudio();
if (contextRef.current) {
contextRef.current.close();
contextRef.current = null;
}
};
}, [stopAudio]);
Copy link

Copilot AI Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cleanup effect on line 96 has stopAudio in its dependency array, which means the cleanup function will be recreated whenever stopAudio changes. This defeats the purpose of having a cleanup-only effect. Consider either removing the dependency array entirely or restructuring to avoid depending on stopAudio.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants