Skip to content

feat: Phase 10 - Over-the-Shoulder Coaching, Voice & Skill Refresh#9

Merged
zackfunprojects merged 2 commits intomainfrom
feat/phase-10-coaching-voice
Apr 14, 2026
Merged

feat: Phase 10 - Over-the-Shoulder Coaching, Voice & Skill Refresh#9
zackfunprojects merged 2 commits intomainfrom
feat/phase-10-coaching-voice

Conversation

@zackfunprojects
Copy link
Copy Markdown
Owner

@zackfunprojects zackfunprojects commented Apr 14, 2026

Summary

  • Over-the-Shoulder coaching: screen capture via getDisplayMedia -> Claude Vision analysis -> real-time coaching points (Pro-gated)
  • Fireside Lessons: voice dialogue with Sherpa via Whisper STT + Claude + ElevenLabs TTS, warm amber UI
  • Voice Response exercises: full implementation replacing stub - record, transcribe, submit for evaluation
  • File Upload exercises: full implementation with Supabase Storage upload + AI evaluation
  • Skill Refresh: spaced retrieval exercises generated from completed trek notebook entries (Pro-gated)
  • 3 new Edge Functions + voice utility library + 3 new components

New Files

File Purpose
supabase/functions/screen-analyze/index.ts Claude Vision OTS coaching (Pro)
supabase/functions/sherpa-voice/index.ts Whisper STT -> Claude -> ElevenLabs TTS
supabase/functions/skill-refresh/index.ts Review exercise generation (Pro)
src/lib/voice.js MediaRecorder, base64, audio playback
src/components/trek/CoachingPanel.jsx OTS slide-in panel with screen capture
src/components/trek/FiresideMode.jsx Voice dialogue overlay
src/components/summit/SkillRefresh.jsx Inline review exercises

External API Requirements

  • OPENAI_API_KEY in Supabase secrets (Whisper STT)
  • ELEVENLABS_API_KEY + ELEVENLABS_VOICE_ID in Supabase secrets (TTS)
  • Voice features degrade gracefully when keys are missing (503 with setup message)

Test plan

  • OTS Coach button visible in LearningView bottom bar
  • CoachingPanel shows Pro gate for free users
  • CoachingPanel captures screen and returns coaching analysis for Pro users
  • Fireside button opens warm amber voice dialogue overlay
  • Voice recording works (MediaRecorder) and transcribes via Whisper
  • Sherpa voice response plays back via ElevenLabs TTS
  • VoiceResponseLedge records, transcribes, submits for exercise evaluation
  • FileUploadLedge uploads to Supabase Storage and submits URL
  • Skill Refresh button on notebook entries generates review exercises (Pro-gated)
  • All features show graceful errors when external APIs unavailable
  • pnpm build and pnpm lint pass clean

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Interactive file upload with 50MB size validation and cloud storage integration.
    • Voice response recording with real-time AI transcription.
    • Skill Refresh exercises for completed trek entries (Pro feature).
    • Coaching Panel with AI-powered screen analysis (Pro feature).
    • Fireside Mode for voice-based conversational learning with audio responses.

Premium teaching modalities: screen-aware coaching, voice dialogue,
file uploads, and spaced skill review for completed treks.

New Edge Functions:
- screen-analyze: Claude Vision analyzes screenshots for OTS coaching (Pro)
- sherpa-voice: Whisper STT -> Claude -> ElevenLabs TTS voice pipeline
- skill-refresh: generates review exercises from notebook entries (Pro)

New frontend:
- voice.js: MediaRecorder, base64 encoding, audio playback utilities
- CoachingPanel: OTS slide-in with screen capture via getDisplayMedia
- FiresideMode: warm amber voice dialogue overlay with Sherpa
- SkillRefresh: inline review exercises on notebook entries
- VoiceResponseLedge: full implementation replacing stub
- FileUploadLedge: full implementation with Supabase Storage upload

Modified:
- sherpa.js: added analyzeScreen, voiceChat, refreshSkill wrappers
- LearningView: OTS Coach and Fireside buttons + panel integration
- TrekNotebookView: Skill Refresh integration per entry
- SummitCard: added Skill Refresh button

External API requirements:
- OPENAI_API_KEY (Whisper STT)
- ELEVENLABS_API_KEY + ELEVENLABS_VOICE_ID (TTS)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 14, 2026

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

Project Deployment Actions Updated (UTC)
altius Ready Ready Preview, Comment Apr 14, 2026 4:20pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 14, 2026

📝 Walkthrough

Walkthrough

This PR adds interactive voice and file-based learning flows, AI-powered coaching analysis, and skill refresh exercises. It includes three new Supabase Edge Functions (screen-analyze, sherpa-voice, skill-refresh), a voice utility library, and updates multiple React components to enable file uploads, voice responses, voice chat, screen-based coaching, and pro-tier skill practice.

Changes

Cohort / File(s) Summary
Ledge Components
src/components/ledges/FileUploadLedge.jsx, src/components/ledges/VoiceResponseLedge.jsx
Transformed from static placeholder components into interactive flows. FileUploadLedge now handles file selection (50MB limit), Supabase Storage upload, and submission callbacks. VoiceResponseLedge supports microphone recording, transcription via voiceChat API, and transcript review before submission.
Summit & Trek Components
src/components/summit/SkillRefresh.jsx, src/components/summit/SummitCard.jsx, src/components/trek/CoachingPanel.jsx, src/components/trek/FiresideMode.jsx
Added two new components (SkillRefresh, CoachingPanel, FiresideMode) with exercise generation, screen capture analysis, and voice conversation flows. Updated SummitCard to include onRefresh callback and "Skill Refresh" button. All components integrate pro-tier subscription checks and AI analysis features.
Voice Utilities
src/lib/voice.js
New module exporting microphone recording (startRecording, stopRecording), audio encoding (blobToBase64), and playback (playAudioBase64) utilities, plus browser support detection (isRecordingSupported).
Sherpa Client Wrappers
src/lib/sherpa.js
Added three new async function wrappers around Supabase Edge Functions: analyzeScreen, voiceChat, and refreshSkill following existing error-handling patterns.
Supabase Edge Functions
supabase/functions/screen-analyze/index.ts, supabase/functions/sherpa-voice/index.ts, supabase/functions/skill-refresh/index.ts
Three new Edge Functions implementing: (1) screenshot analysis for coaching via Anthropic, (2) voice transcription + AI response + optional TTS via OpenAI/Claude/ElevenLabs, (3) skill refresh exercise generation via Claude with pro-tier gating. All include auth validation, rate limiting, and error handling.
View Integration
src/views/LearningView.jsx, src/views/TrekNotebookView.jsx
Integrated new components into learning and notebook views. LearningView adds CoachingPanel and FiresideMode toggles; TrekNotebookView adds SkillRefresh state management and conditional rendering beneath SummitCard entries.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant VRL as VoiceResponseLedge
    participant VM as Voice Module
    participant SC as Sherpa Client
    participant SF as sherpa-voice<br/>Edge Function
    participant OpenAI as OpenAI Whisper
    participant Claude as Claude API
    
    User->>VRL: Click Record
    VRL->>VM: startRecording()
    VM->>VM: Request microphone access
    User->>VRL: Speak & Click Stop
    VRL->>VM: stopRecording()
    VM->>VM: Encode audio to base64
    VM-->>VRL: base64 audio
    VRL->>SC: voiceChat(audioBase64)
    SC->>SF: POST sherpa-voice
    SF->>OpenAI: Transcribe audio
    OpenAI-->>SF: transcript
    SF->>Claude: Generate response<br/>(with conversation history)
    Claude-->>SF: response_text
    SF->>SF: Optional ElevenLabs TTS
    SF-->>SC: { transcript, response_text, audio_base64 }
    SC-->>VRL: response data
    VRL->>VRL: Append to messages
    alt audio_base64 present
        VRL->>VM: playAudioBase64()
        VM->>VM: Play via Audio element
    end
    VRL-->>User: Display transcript & play response
Loading
sequenceDiagram
    actor User
    participant CP as CoachingPanel
    participant SC as Sherpa Client
    participant SF as screen-analyze<br/>Edge Function
    participant MediaAPI as navigator<br/>mediaDevices
    participant Claude as Claude API
    
    User->>CP: Click "Share Screen<br/>for Coaching"
    CP->>MediaAPI: getDisplayMedia()
    User->>MediaAPI: Select screen
    MediaAPI-->>CP: MediaStream
    CP->>CP: Capture frame via<br/>ImageCapture
    CP->>CP: Convert to base64 PNG
    CP->>SC: analyzeScreen(screenshot)
    SC->>SF: POST screen-analyze
    SF->>SF: Validate auth & pro-tier
    SF->>Claude: Analyze screenshot<br/>with context
    Claude-->>SF: { analysis,<br/>coaching_points,<br/>suggestion }
    SF-->>SC: analysis data
    SC-->>CP: analysis result
    CP->>CP: Stop media tracks
    CP-->>User: Display analysis,<br/>coaching points,<br/>suggestion
Loading
sequenceDiagram
    actor User
    participant SR as SkillRefresh
    participant SC as Sherpa Client
    participant SF as skill-refresh<br/>Edge Function
    participant Claude as Claude API
    participant DB as Supabase
    
    User->>SR: Click "Start Refresh"
    SR->>SC: refreshSkill(notebookEntryId)
    SC->>SF: POST skill-refresh
    SF->>DB: Fetch notebook entry
    DB-->>SF: entry data
    SF->>SF: Validate ownership<br/>& pro-tier
    SF->>Claude: Generate 3–5<br/>exercise prompts
    Claude-->>SF: { exercises: [...] }
    SF->>DB: Update<br/>last_refreshed_at
    SF-->>SC: exercises array
    SC-->>SR: exercises data
    SR->>SR: Initialize currentIndex=0
    loop For each exercise
        User->>SR: Answer question
        SR->>SR: Store answer
        User->>SR: Click Next
        SR->>SR: Advance currentIndex
    end
    User->>SR: Click Finish
    SR-->>User: Close / Done
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰 Chirp chirp! A tale of voice and screens—
Files hop through storage, transcripts gleam,
With coaching whispers and refreshed skill trees,
The rabbit rejoices at features so keen! 📝✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: introduction of three major features (Over-the-Shoulder Coaching, Voice dialogue, and Skill Refresh), which directly align with the primary additions in the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/phase-10-coaching-voice

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🧹 Nitpick comments (2)
src/components/trek/FiresideMode.jsx (1)

20-25: Consider resetting conversation state when panel closes.

The cleanup effect stops the recorder but doesn't reset messages, error, or other state. When the user reopens the Fireside panel, the previous conversation persists. If this is intentional (session continuity), this is fine. If each session should start fresh, add state resets.

♻️ Optional: Reset state on close
   // Cleanup recorder on close
   useEffect(() => {
-    if (!open && recorderRef?.state === 'recording') {
-      recorderRef.stop()
+    if (!open) {
+      if (recorderRef?.state === 'recording') {
+        recorderRef.stop()
+      }
+      // Uncomment if fresh session desired:
+      // setMessages([])
+      // setError(null)
     }
   }, [open, recorderRef])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/trek/FiresideMode.jsx` around lines 20 - 25, The cleanup
effect that watches open and recorderRef currently only stops the recorder but
doesn't clear conversation state; update the effect (the useEffect that checks
if (!open && recorderRef?.state === 'recording')) to also reset component state
on panel close by calling the relevant setters such as setMessages([]),
setError(null) and any other session-specific setters you have (e.g.,
setIsRecording(false), setConversationId(null) or similar) so that reopening
FiresideMode starts a fresh session; keep the recorder stop behavior and ensure
these resets only run when open becomes false.
src/components/summit/SkillRefresh.jsx (1)

145-164: Short answer allows empty submissions.

Once a user types anything in the short answer input (even if they clear it), answers[currentIndex] becomes an empty string. Since '' !== undefined is true, the "Next" button appears, allowing users to proceed with a blank answer. Consider requiring non-empty input.

♻️ Proposed fix to require non-empty answers
       {/* Next button */}
-      {answers[currentIndex] !== undefined && (
+      {(answers[currentIndex] !== undefined && 
+        (currentExercise.type !== 'short_answer' || answers[currentIndex]?.trim())) && (
         <button
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/summit/SkillRefresh.jsx` around lines 145 - 164, The
Next/Finish button is shown when answers[currentIndex] !== undefined which
treats an empty string as valid; change the visibility check to require a
non-empty, non-whitespace answer (e.g. test answers[currentIndex] exists and
answers[currentIndex].toString().trim().length > 0) so blank submissions are
disabled; update the conditional that renders the button (and any related logic
in handleNext/onClose if they assume non-empty) and reference answers,
currentIndex, handleAnswer, handleNext, onClose, currentExercise.type and
exercises to locate the code to modify.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/ledges/FileUploadLedge.jsx`:
- Around line 40-42: The ext extraction in FileUploadLedge.jsx currently uses
file.name.split('.').pop() which returns the whole filename for names without
extensions (e.g., "Makefile") or mishandles dotfiles; update the logic inside
the try block where ext and path are created so ext is derived only when there
is a true extension (use file.name.lastIndexOf('.') or a /\.([^.]+)$/ regex to
capture the suffix) and fall back to a safe default like 'bin' otherwise, then
build path using that ext (the existing path template using user.id and
Date.now. should remain unchanged).
- Line 11: FileUploadLedge currently destructures { spec, onSubmit } and calls
onSubmit(...) which mismatches ExerciseWrapper (which passes onResponseChange)
and causes runtime undefined; change the prop name to onResponseChange in the
component signature and all internal uses (replace onSubmit references,
including the call in handleSubmit) to onResponseChange to match other ledges
(ConversationSimLedge, VoiceResponseLedge), and also accept and use the disabled
prop (e.g., prevent submission or disable inputs when disabled) so the passed
disabled flag is honored.

In `@src/components/trek/CoachingPanel.jsx`:
- Around line 97-99: In CoachingPanel.jsx update the copy inside the paragraph
element (the <p className="font-mono text-phosphor-green/40 text-xs"> node) used
in the Pro gate message: replace the incorrect phrase "real tools" with "real
time" so the sentence reads "The Sherpa watches your screen in real time and
coaches live." Keep everything else (className and markup) unchanged.
- Around line 25-31: The ImageCapture usage can throw in browsers without
support (e.g., Firefox); update the section that calls
navigator.mediaDevices.getDisplayMedia, extracts track =
stream.getVideoTracks()[0], creates new ImageCapture(track) and calls
imageCapture.grabFrame() to first feature-detect support and wrap the
ImageCapture creation/grabFrame call in a try-catch; on failure fall back to
capturing a frame from a hidden video element/canvas using the MediaStream (or
show a clear user-facing error) and ensure the track is stopped on error so
resources are released.

In `@src/components/trek/FiresideMode.jsx`:
- Around line 183-185: The UI copy in FiresideMode.jsx is incorrect: the
paragraph using the conditional expression with recording and processing
currently shows "Hold to speak" but the component uses click-to-toggle
start/stop handlers (not press-and-hold). Update the text to reflect click/tap
behavior (for example "Click to speak" or "Tap to speak" as appropriate) in the
JSX where the expression {recording ? 'Release to send' : processing ?
'Processing...' : 'Hold to speak'} is defined so the default branch matches the
actual toggle interaction; keep the existing conditional logic but replace the
"Hold to speak" string with the correct wording.

In `@src/lib/voice.js`:
- Around line 21-25: The MediaRecorder fallback is unsafe: update
isRecordingSupported() to check MediaRecorder API existence AND probe a
prioritized list of MIME types via MediaRecorder.isTypeSupported (e.g.,
'audio/webm;codecs=opus', 'audio/webm', 'audio/ogg;codecs=opus', 'audio/wav')
and return true only if one is supported; when creating the recorder in
startRecording() select the first supported MIME and store it (e.g.,
selectedMime) alongside the Blob, and send that MIME to the backend with the
upload (e.g., as a form field or an HTTP header); then update the backend
handler in sherpa-voice/index.ts to accept the provided MIME type parameter and
use it instead of hardcoding 'audio/webm' (and validate it against an allowlist)
so the server correctly processes the actual format recorded.
- Around line 86-107: The playAudioBase64 function currently ignores the Promise
returned by audio.play(), leaving playAudioBase64 unresolved on playback
rejection; update playAudioBase64 to capture the return value of audio.play(),
check if it's a Promise, and attach .then/.catch handlers that on success do
nothing (playback proceeds) and on rejection revoke the object URL and reject
the outer Promise with the error; ensure existing audio.onended and
audio.onerror still revoke the URL and resolve/reject, and guard against
double-settlement by only resolving/rejecting once (e.g., via a local settled
flag) so legacy browsers that return undefined still work.

In `@supabase/functions/screen-analyze/index.ts`:
- Around line 137-138: The catch block in
supabase/functions/screen-analyze/index.ts currently logs part of Claude’s
response via console.error("Failed to parse screen analysis:", text.slice(0,
300)); remove any direct logging of the variable text (which may contain PII)
and instead log only a safe diagnostic such as the caught error object/message
and a non-sensitive metric (e.g., text length or "redacted response"); update
the catch to accept the error parameter and call console.error with a generic
message and error (but not the response content) so you no longer write
screen-derived model output to logs.
- Around line 130-147: The JSON parsing block currently assigns
JSON.parse(cleaned) directly to result and then assumes result is an object;
instead, parse into a local const (e.g., parsed = JSON.parse(cleaned)), validate
that parsed is non-null, typeof parsed === "object", and Array.isArray(parsed)
=== false, and if validation fails throw a new Error("Expected JSON object") so
the existing catch handles it; after validation assign parsed to result (e.g.,
result = parsed as Record<string, unknown>) and then continue with the existing
normalization of result.analysis, result.coaching_points, and result.suggestion
to avoid runtime errors when Claude returns an array or scalar.

In `@supabase/functions/sherpa-voice/index.ts`:
- Around line 91-171: Add a transcript-only short-circuit: read a boolean flag
from the incoming request body (e.g., transcriptOnly or transcript_only) and if
true immediately perform the minimal transcript upload/DB update and return the
transcript JSON response without calling anthropic.messages.create
(callWithRetry/anthropic block) or invoking the ElevenLabs TTS fetch block;
update the handler in this file (around the code that constructs messages, the
callWithRetry(...) anthropic call, and the TTS block) to check the flag before
building messages/trek context and before the ElevenLabs section so those
expensive calls are skipped when only the transcript is needed.
- Around line 107-116: The trek lookup uses the service-role Supabase client and
only filters by trek_id, allowing any caller to inject arbitrary trek data into
contextBlock; fix it by enforcing authorization: retrieve the caller's
authenticated user (e.g., via supabase.auth.getUser() or using an authenticated
client) and modify the query to ensure the trek belongs to or is visible to that
user (for example add .eq('owner_id', user.id) or .or('is_public.eq.true') to
the supabase.from("treks").select(...).eq("id", trek_id) call) before setting
contextBlock so only authorized trek data is included.

In `@supabase/functions/skill-refresh/index.ts`:
- Around line 124-139: Ensure the parsed model payload is an object before
mutating it: after JSON.parse(text) (the local variable result) check that
typeof result === "object" && result !== null && !Array.isArray(result); if the
check fails, log the offending payload (use text.slice(0,300) or the parsed
value) and return the same 500 Response used for parse failures instead of
proceeding to set result.exercises = ..., to avoid treating arrays or scalars as
valid objects.
- Around line 141-146: The update of last_refreshed_at via
supabase.from("trek_notebook").update(...) currently ignores the response and
lets the function return exercises even on failure; modify the code to check the
Supabase response (error and status) after the update of last_refreshed_at (and
use the same notebook_entry_id and authUserId used in the .eq calls), and if an
error is returned, log/record the error and stop the flow (return an error
response or throw) so exercises are not returned when the persistence fails;
ensure the error branch returns a clear failure to the caller and includes the
error details in the log/response.

---

Nitpick comments:
In `@src/components/summit/SkillRefresh.jsx`:
- Around line 145-164: The Next/Finish button is shown when
answers[currentIndex] !== undefined which treats an empty string as valid;
change the visibility check to require a non-empty, non-whitespace answer (e.g.
test answers[currentIndex] exists and
answers[currentIndex].toString().trim().length > 0) so blank submissions are
disabled; update the conditional that renders the button (and any related logic
in handleNext/onClose if they assume non-empty) and reference answers,
currentIndex, handleAnswer, handleNext, onClose, currentExercise.type and
exercises to locate the code to modify.

In `@src/components/trek/FiresideMode.jsx`:
- Around line 20-25: The cleanup effect that watches open and recorderRef
currently only stops the recorder but doesn't clear conversation state; update
the effect (the useEffect that checks if (!open && recorderRef?.state ===
'recording')) to also reset component state on panel close by calling the
relevant setters such as setMessages([]), setError(null) and any other
session-specific setters you have (e.g., setIsRecording(false),
setConversationId(null) or similar) so that reopening FiresideMode starts a
fresh session; keep the recorder stop behavior and ensure these resets only run
when open becomes false.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4aed1183-1bfe-4d5b-a839-6dd9b4643605

📥 Commits

Reviewing files that changed from the base of the PR and between 0b3e899 and b5b7d6d.

📒 Files selected for processing (13)
  • src/components/ledges/FileUploadLedge.jsx
  • src/components/ledges/VoiceResponseLedge.jsx
  • src/components/summit/SkillRefresh.jsx
  • src/components/summit/SummitCard.jsx
  • src/components/trek/CoachingPanel.jsx
  • src/components/trek/FiresideMode.jsx
  • src/lib/sherpa.js
  • src/lib/voice.js
  • src/views/LearningView.jsx
  • src/views/TrekNotebookView.jsx
  • supabase/functions/screen-analyze/index.ts
  • supabase/functions/sherpa-voice/index.ts
  • supabase/functions/skill-refresh/index.ts

/**
* File upload exercise: upload a creative deliverable for AI evaluation.
*/
export default function FileUploadLedge({ spec, onSubmit }) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify how FileUploadLedge is used across the codebase
rg -n "FileUploadLedge" --type=js --type=jsx -A 2 -B 2

Repository: zackfunprojects/altius

Length of output: 93


🏁 Script executed:

#!/bin/bash
# Find FileUploadLedge and ExerciseWrapper files
fd -t f "FileUploadLedge" || fd -t f "ExerciseWrapper"

Repository: zackfunprojects/altius

Length of output: 107


🏁 Script executed:

#!/bin/bash
# Search for FileUploadLedge usage without specifying jsx type
rg -n "FileUploadLedge" -A 2 -B 2

Repository: zackfunprojects/altius

Length of output: 2462


🏁 Script executed:

#!/bin/bash
# Search for ExerciseWrapper to find the file
fd -e js -e jsx | xargs grep -l "ExerciseWrapper" | head -5

Repository: zackfunprojects/altius

Length of output: 149


🏁 Script executed:

#!/bin/bash
# Read FileUploadLedge to see onSubmit usage
cat -n src/components/ledges/FileUploadLedge.jsx

Repository: zackfunprojects/altius

Length of output: 5999


🏁 Script executed:

#!/bin/bash
# Read ExerciseWrapper to see how FileUploadLedge is used and handleResponseChange
sed -n '160,180p' src/components/exercises/ExerciseWrapper.jsx

Repository: zackfunprojects/altius

Length of output: 764


🏁 Script executed:

#!/bin/bash
# Check if there are any other usages of FileUploadLedge
rg "FileUploadLedge" -c

Repository: zackfunprojects/altius

Length of output: 201


Prop mismatch: ExerciseWrapper passes onResponseChange, but this component expects onSubmit.

FileUploadLedge is destructuring { spec, onSubmit } on line 11 and calling onSubmit(...) on line 64. However, ExerciseWrapper passes onResponseChange instead (line 172 of ExerciseWrapper.jsx), causing the submit functionality to fail with an undefined function error at runtime.

Align the component signature with other ledges (ConversationSimLedge, VoiceResponseLedge) that all accept onResponseChange:

-export default function FileUploadLedge({ spec, onSubmit }) {
+export default function FileUploadLedge({ spec, onResponseChange }) {

Then update the handleSubmit call:

   const handleSubmit = useCallback(() => {
     if (!uploadedUrl) return
-    onSubmit({
+    onResponseChange({
       file_url: uploadedUrl,
       file_name: file?.name,
       file_type: file?.type,
       type: 'file_upload',
     })
-  }, [uploadedUrl, file, onSubmit])
+  }, [uploadedUrl, file, onResponseChange])

Also note: the disabled prop is being passed but not used in the component.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ledges/FileUploadLedge.jsx` at line 11, FileUploadLedge
currently destructures { spec, onSubmit } and calls onSubmit(...) which
mismatches ExerciseWrapper (which passes onResponseChange) and causes runtime
undefined; change the prop name to onResponseChange in the component signature
and all internal uses (replace onSubmit references, including the call in
handleSubmit) to onResponseChange to match other ledges (ConversationSimLedge,
VoiceResponseLedge), and also accept and use the disabled prop (e.g., prevent
submission or disable inputs when disabled) so the passed disabled flag is
honored.

Comment on lines +40 to +42
try {
const ext = file.name.split('.').pop() || 'bin'
const path = `exercise-files/${user.id}/${Date.now()}.${ext}`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Edge case: Extension extraction fails for files without extensions.

If a file has no extension (e.g., "Makefile" or "README"), split('.').pop() returns the entire filename, creating a path like 1713100000000.Makefile. Consider a more robust extraction.

🛡️ Proposed fix
-      const ext = file.name.split('.').pop() || 'bin'
+      const parts = file.name.split('.')
+      const ext = parts.length > 1 ? parts.pop() : 'bin'
       const path = `exercise-files/${user.id}/${Date.now()}.${ext}`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ledges/FileUploadLedge.jsx` around lines 40 - 42, The ext
extraction in FileUploadLedge.jsx currently uses file.name.split('.').pop()
which returns the whole filename for names without extensions (e.g., "Makefile")
or mishandles dotfiles; update the logic inside the try block where ext and path
are created so ext is derived only when there is a true extension (use
file.name.lastIndexOf('.') or a /\.([^.]+)$/ regex to capture the suffix) and
fall back to a safe default like 'bin' otherwise, then build path using that ext
(the existing path template using user.id and Date.now. should remain
unchanged).

Comment thread src/components/trek/CoachingPanel.jsx Outdated
Comment on lines +25 to +31
const stream = await navigator.mediaDevices.getDisplayMedia({
video: { mediaSource: 'screen' },
})

const track = stream.getVideoTracks()[0]
const imageCapture = new ImageCapture(track)
const bitmap = await imageCapture.grabFrame()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

ImageCapture API has limited browser support and may throw in Firefox.

The ImageCapture API is not supported in Firefox and some other browsers. This will cause an unhandled error when a Firefox user (even with Pro) attempts to use the feature. Consider adding feature detection or a try-catch specifically around ImageCapture usage, with a user-friendly error message.

🛡️ Proposed fix to add feature detection
       const stream = await navigator.mediaDevices.getDisplayMedia({
-        video: { mediaSource: 'screen' },
+        video: true,
       })

       const track = stream.getVideoTracks()[0]
+      
+      // Check for ImageCapture support (not available in Firefox)
+      if (typeof ImageCapture === 'undefined') {
+        stream.getTracks().forEach((t) => t.stop())
+        throw new Error('Screen capture analysis is not supported in this browser.')
+      }
+      
       const imageCapture = new ImageCapture(track)
Does Firefox support the ImageCapture API?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/trek/CoachingPanel.jsx` around lines 25 - 31, The ImageCapture
usage can throw in browsers without support (e.g., Firefox); update the section
that calls navigator.mediaDevices.getDisplayMedia, extracts track =
stream.getVideoTracks()[0], creates new ImageCapture(track) and calls
imageCapture.grabFrame() to first feature-detect support and wrap the
ImageCapture creation/grabFrame call in a try-catch; on failure fall back to
capturing a frame from a hidden video element/canvas using the MediaStream (or
show a clear user-facing error) and ensure the track is stopped on error so
resources are released.

Comment on lines +97 to +99
<p className="font-mono text-phosphor-green/40 text-xs">
The Sherpa watches your screen in real tools and coaches live.
</p>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Typo: "real tools" should be "real time".

Minor copy error in the Pro gate message.

📝 Proposed fix
               <p className="font-mono text-phosphor-green/40 text-xs">
-                The Sherpa watches your screen in real tools and coaches live.
+                The Sherpa watches your screen in real time and coaches live.
               </p>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<p className="font-mono text-phosphor-green/40 text-xs">
The Sherpa watches your screen in real tools and coaches live.
</p>
<p className="font-mono text-phosphor-green/40 text-xs">
The Sherpa watches your screen in real time and coaches live.
</p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/trek/CoachingPanel.jsx` around lines 97 - 99, In
CoachingPanel.jsx update the copy inside the paragraph element (the <p
className="font-mono text-phosphor-green/40 text-xs"> node) used in the Pro gate
message: replace the incorrect phrase "real tools" with "real time" so the
sentence reads "The Sherpa watches your screen in real time and coaches live."
Keep everything else (className and markup) unchanged.

Comment on lines +183 to +185
<p className="font-ui text-catalog-cream/40 text-xs">
{recording ? 'Release to send' : processing ? 'Processing...' : 'Hold to speak'}
</p>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

UI text "Hold to speak" is misleading - this is click-to-toggle, not press-and-hold.

The button uses onClick handlers for start/stop, meaning users click once to start and click again to stop. The text "Hold to speak" implies press-and-hold behavior which doesn't match the implementation. This could confuse users.

📝 Proposed fix
             <p className="font-ui text-catalog-cream/40 text-xs">
-              {recording ? 'Release to send' : processing ? 'Processing...' : 'Hold to speak'}
+              {recording ? 'Tap to send' : processing ? 'Processing...' : 'Tap to speak'}
             </p>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<p className="font-ui text-catalog-cream/40 text-xs">
{recording ? 'Release to send' : processing ? 'Processing...' : 'Hold to speak'}
</p>
<p className="font-ui text-catalog-cream/40 text-xs">
{recording ? 'Tap to send' : processing ? 'Processing...' : 'Tap to speak'}
</p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/trek/FiresideMode.jsx` around lines 183 - 185, The UI copy in
FiresideMode.jsx is incorrect: the paragraph using the conditional expression
with recording and processing currently shows "Hold to speak" but the component
uses click-to-toggle start/stop handlers (not press-and-hold). Update the text
to reflect click/tap behavior (for example "Click to speak" or "Tap to speak" as
appropriate) in the JSX where the expression {recording ? 'Release to send' :
processing ? 'Processing...' : 'Hold to speak'} is defined so the default branch
matches the actual toggle interaction; keep the existing conditional logic but
replace the "Hold to speak" string with the correct wording.

Comment on lines +137 to +138
} catch {
console.error("Failed to parse screen analysis:", text.slice(0, 300));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep screen-derived model output out of logs.

On parse failures this writes part of Claude’s response to server logs. Because that response is based on a screenshot, it can easily echo secrets or PII visible on the user’s screen.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/screen-analyze/index.ts` around lines 137 - 138, The catch
block in supabase/functions/screen-analyze/index.ts currently logs part of
Claude’s response via console.error("Failed to parse screen analysis:",
text.slice(0, 300)); remove any direct logging of the variable text (which may
contain PII) and instead log only a safe diagnostic such as the caught error
object/message and a non-sensitive metric (e.g., text length or "redacted
response"); update the catch to accept the error parameter and call
console.error with a generic message and error (but not the response content) so
you no longer write screen-derived model output to logs.

Comment on lines +91 to +171
// Step 2: Get Sherpa response via Claude
// Build context messages from conversation history
const messages: Array<{ role: string; content: string }> = [];

if (Array.isArray(conversation_history)) {
for (const msg of conversation_history.slice(-10)) {
if (msg.role && msg.content) {
messages.push({ role: msg.role, content: msg.content.slice(0, 2000) });
}
}
}

messages.push({ role: "user", content: transcript });

// Fetch trek context for Sherpa
let contextBlock = "";
if (trek_id) {
const { data: trek } = await supabase
.from("treks")
.select("trek_name, skill_description, difficulty")
.eq("id", trek_id)
.single();

if (trek) {
contextBlock = `\n\nTrek context: ${trek.trek_name} (${trek.difficulty}) - ${trek.skill_description}`;
}
}

const sherpaResponse = await callWithRetry(() =>
anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 512,
system: `${SHERPA_SYSTEM_PROMPT}

You are in Fireside Lesson mode - a spoken conversation with the climber. Keep responses concise and conversational (2-4 sentences). Speak naturally as you would around a campfire. Do not use markdown formatting, bullet points, or code blocks - this will be read aloud.${contextBlock}`,
messages: messages as Array<{ role: "user" | "assistant"; content: string }>,
})
);

const responseText =
sherpaResponse.content?.[0]?.type === "text" ? sherpaResponse.content[0].text : "";

// Step 3: Synthesize speech via ElevenLabs (optional)
let audioResponseBase64 = null;

if (elevenlabsApiKey && responseText) {
try {
const ttsResponse = await fetch(
`https://api.elevenlabs.io/v1/text-to-speech/${elevenlabsVoiceId}`,
{
method: "POST",
headers: {
"xi-api-key": elevenlabsApiKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
text: responseText,
model_id: "eleven_monolingual_v1",
voice_settings: {
stability: 0.6,
similarity_boost: 0.75,
},
}),
}
);

if (ttsResponse.ok) {
const audioBuffer = await ttsResponse.arrayBuffer();
const bytes = new Uint8Array(audioBuffer);
let binary = "";
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
audioResponseBase64 = btoa(binary);
} else {
console.error("ElevenLabs TTS error:", await ttsResponse.text());
}
} catch (ttsErr) {
console.error("TTS synthesis failed:", (ttsErr as Error).message);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add a transcript-only path for voice-response uploads.

src/components/ledges/VoiceResponseLedge.jsx calls this endpoint just to get transcript, but this handler always continues on to Claude and may also hit ElevenLabs. That makes every voice-response submission slower and more expensive than necessary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/sherpa-voice/index.ts` around lines 91 - 171, Add a
transcript-only short-circuit: read a boolean flag from the incoming request
body (e.g., transcriptOnly or transcript_only) and if true immediately perform
the minimal transcript upload/DB update and return the transcript JSON response
without calling anthropic.messages.create (callWithRetry/anthropic block) or
invoking the ElevenLabs TTS fetch block; update the handler in this file (around
the code that constructs messages, the callWithRetry(...) anthropic call, and
the TTS block) to check the flag before building messages/trek context and
before the ElevenLabs section so those expensive calls are skipped when only the
transcript is needed.

Comment on lines +107 to +116
if (trek_id) {
const { data: trek } = await supabase
.from("treks")
.select("trek_name, skill_description, difficulty")
.eq("id", trek_id)
.single();

if (trek) {
contextBlock = `\n\nTrek context: ${trek.trek_name} (${trek.difficulty}) - ${trek.skill_description}`;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Authorize the trek lookup before injecting it into the prompt.

This uses the service-role client and filters only by id. Any authenticated caller who knows another trek UUID can load that trek’s name, skill description, and difficulty into Claude’s context.

🔒 Suggested fix
-      const { data: trek } = await supabase
+      const { data: trek, error: trekError } = await supabase
         .from("treks")
         .select("trek_name, skill_description, difficulty")
         .eq("id", trek_id)
+        .eq("user_id", authUserId)
         .single();
 
-      if (trek) {
+      if (trekError) {
+        return new Response(
+          JSON.stringify({ error: "Access denied" }),
+          { status: 403, headers: { ...cors, "Content-Type": "application/json" } }
+        );
+      }
+
+      if (trek) {
         contextBlock = `\n\nTrek context: ${trek.trek_name} (${trek.difficulty}) - ${trek.skill_description}`;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/sherpa-voice/index.ts` around lines 107 - 116, The trek
lookup uses the service-role Supabase client and only filters by trek_id,
allowing any caller to inject arbitrary trek data into contextBlock; fix it by
enforcing authorization: retrieve the caller's authenticated user (e.g., via
supabase.auth.getUser() or using an authenticated client) and modify the query
to ensure the trek belongs to or is visible to that user (for example add
.eq('owner_id', user.id) or .or('is_public.eq.true') to the
supabase.from("treks").select(...).eq("id", trek_id) call) before setting
contextBlock so only authorized trek data is included.

Comment on lines +124 to +139
let result;
try {
const cleaned = text
.replace(/```json?\s*/g, "")
.replace(/```\s*/g, "")
.trim();
result = JSON.parse(cleaned);
} catch {
console.error("Failed to parse skill refresh:", text.slice(0, 300));
return new Response(
JSON.stringify({ error: "Failed to generate refresh exercises. Please try again." }),
{ status: 500, headers: { ...cors, "Content-Type": "application/json" } }
);
}

result.exercises = Array.isArray(result.exercises) ? result.exercises : [];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reject non-object model payloads before mutating result.

A bare array or scalar is still valid JSON. Here that either returns the wrong wire shape or throws at result.exercises = ..., which turns schema drift into a generic 500.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/skill-refresh/index.ts` around lines 124 - 139, Ensure the
parsed model payload is an object before mutating it: after JSON.parse(text)
(the local variable result) check that typeof result === "object" && result !==
null && !Array.isArray(result); if the check fails, log the offending payload
(use text.slice(0,300) or the parsed value) and return the same 500 Response
used for parse failures instead of proceeding to set result.exercises = ..., to
avoid treating arrays or scalars as valid objects.

Comment on lines +141 to +146
// Update last_refreshed_at
await supabase
.from("trek_notebook")
.update({ last_refreshed_at: new Date().toISOString() })
.eq("id", notebook_entry_id)
.eq("user_id", authUserId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t ignore failures when persisting last_refreshed_at.

The function returns exercises even if this update fails, which silently breaks the spacing signal the feature depends on.

🛠️ Suggested fix
-    await supabase
+    const { error: updateError } = await supabase
       .from("trek_notebook")
       .update({ last_refreshed_at: new Date().toISOString() })
       .eq("id", notebook_entry_id)
       .eq("user_id", authUserId);
+
+    if (updateError) {
+      console.error("Failed to update last_refreshed_at:", updateError.message);
+      return new Response(
+        JSON.stringify({ error: "Failed to save refresh state. Please try again." }),
+        { status: 500, headers: { ...cors, "Content-Type": "application/json" } }
+      );
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Update last_refreshed_at
await supabase
.from("trek_notebook")
.update({ last_refreshed_at: new Date().toISOString() })
.eq("id", notebook_entry_id)
.eq("user_id", authUserId);
// Update last_refreshed_at
const { error: updateError } = await supabase
.from("trek_notebook")
.update({ last_refreshed_at: new Date().toISOString() })
.eq("id", notebook_entry_id)
.eq("user_id", authUserId);
if (updateError) {
console.error("Failed to update last_refreshed_at:", updateError.message);
return new Response(
JSON.stringify({ error: "Failed to save refresh state. Please try again." }),
{ status: 500, headers: { ...cors, "Content-Type": "application/json" } }
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/skill-refresh/index.ts` around lines 141 - 146, The update
of last_refreshed_at via supabase.from("trek_notebook").update(...) currently
ignores the response and lets the function return exercises even on failure;
modify the code to check the Supabase response (error and status) after the
update of last_refreshed_at (and use the same notebook_entry_id and authUserId
used in the .eq calls), and if an error is returned, log/record the error and
stop the flow (return an error response or throw) so exercises are not returned
when the persistence fails; ensure the error branch returns a clear failure to
the caller and includes the error details in the log/response.

Edge Functions:
- screen-analyze: validate JSON shape (reject arrays/scalars), remove PII from logs
- sherpa-voice: add trek ownership check (.eq user_id), add transcribe_only mode
  for VoiceResponseLedge (skips Claude + TTS), accept transcribe_only param
- skill-refresh: validate JSON shape, handle last_refreshed_at update error

Frontend:
- voice.js: broader MIME type support (mp4, ogg fallbacks), safer playback error handling
- CoachingPanel: fallback for Firefox (no ImageCapture API), fix "real tools" -> "real time" typo
- FiresideMode: fix "Hold to speak" -> "Click to speak" (matches click-to-toggle behavior)
- FileUploadLedge: robust extension extraction for extensionless files
- VoiceResponseLedge: use transcribe_only mode (faster, cheaper)
- SkillRefresh: require non-empty short answer before allowing Next
- sherpa.js: pass transcribeOnly param through voiceChat wrapper

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant