diff --git a/src/stores/transcriptionStore.ts b/src/stores/transcriptionStore.ts index 25455bb..c65137f 100644 --- a/src/stores/transcriptionStore.ts +++ b/src/stores/transcriptionStore.ts @@ -130,6 +130,11 @@ interface TranscriptionState { keywordWatches: KeywordWatch[]; keywordMatches: KeywordMatch[]; + // Auth (set by panel via setAuthInfo after SIGNED_IN) + _accessToken: string | null; + _userId: string | null; + setAuthInfo: (token: string | null, userId: string | null) => void; + // Actions startSession: (timetableTag?: TimetablePeriod) => Promise; stopSession: () => Promise; @@ -167,6 +172,8 @@ export const useTranscriptionStore = create((set, get) => ({ isRecording: false, isConnecting: false, activeSession: null, + _accessToken: null, + _userId: null, completedSegments: [], serverWindow: [], currentSegment: null, @@ -195,19 +202,23 @@ export const useTranscriptionStore = create((set, get) => ({ set({ timetableContext: context }); }, + setAuthInfo: (token: string | null, userId: string | null) => { + set({ _accessToken: token, _userId: userId }); + }, + startSession: async (timetableTag?: TimetablePeriod) => { set({ isRecording: true, isConnecting: false, elapsedSeconds: 0, timetableContext: timetableTag || null }); // Create session in Supabase try { - const { data: sessionData_auth } = await supabase.auth.getSession(); - if (!sessionData_auth.session?.user) { + const { _accessToken: token, _userId: userId } = get(); + if (!token || !userId) { console.error('No authenticated user'); return; } const sessionData = { - user_id: sessionData_auth.session.user.id, + user_id: userId, title: timetableTag?.event_label || 'Untitled Session', canvas_type: 'teaching-canvas', timetable_period_id: timetableTag?.period_id || null, @@ -465,7 +476,7 @@ export const useTranscriptionStore = create((set, get) => ({ for (const event of eventsToFlush) { await supabase.from('canvas_events').insert({ session_id: activeSession?.id || null, - user_id: (await supabase.auth.getSession()).data.session?.user?.id || '', + user_id: get()._userId || '', timestamp: new Date().toISOString(), session_elapsed_seconds: event.sessionElapsedSeconds || null, event_type: event.eventType, @@ -484,13 +495,13 @@ export const useTranscriptionStore = create((set, get) => ({ loadSessions: async (): Promise => { try { - const { data: sessionData_auth } = await supabase.auth.getSession(); - if (!sessionData_auth.session?.user) return []; + const { _userId: userId } = get(); + if (!userId) return []; const { data, error } = await supabase .from('transcription_sessions') .select('*') - .eq('user_id', sessionData_auth.session.user.id) + .eq('user_id', userId) .order('started_at', { ascending: false }) .limit(50); @@ -584,11 +595,11 @@ export const useTranscriptionStore = create((set, get) => ({ loadKeywordWatches: async () => { try { - const { data: { session } } = await supabase.auth.getSession(); - if (!session?.access_token) return; + const { _accessToken: token } = get(); + if (!token) return; const apiBaseUrl = import.meta.env.VITE_API_BASE || 'https://api.classroomcopilot.ai'; const response = await fetch(`${apiBaseUrl}/transcribe/keywords`, { - headers: { 'Authorization': `Bearer ${session.access_token}` }, + headers: { 'Authorization': `Bearer ${token}` }, }); if (!response.ok) return; const watches = await response.json(); @@ -600,17 +611,17 @@ export const useTranscriptionStore = create((set, get) => ({ addKeywordWatch: async (keyword: string) => { try { - const { data: { session } } = await supabase.auth.getSession(); - if (!session?.access_token || !session.user) return; + const { _accessToken: token, _userId: userId } = get(); + if (!token || !userId) return; const apiBaseUrl = import.meta.env.VITE_API_BASE || 'https://api.classroomcopilot.ai'; const response = await fetch(`${apiBaseUrl}/transcribe/keywords`, { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${session.access_token}`, + 'Authorization': `Bearer ${token}`, }, body: JSON.stringify({ - user_id: session.user.id, + user_id: userId, keyword: keyword.trim(), match_type: 'contains', action: 'alert', @@ -626,12 +637,12 @@ export const useTranscriptionStore = create((set, get) => ({ deleteKeywordWatch: async (watchId: string) => { try { - const { data: { session } } = await supabase.auth.getSession(); - if (!session?.access_token) return; + const { _accessToken: token } = get(); + if (!token) return; const apiBaseUrl = import.meta.env.VITE_API_BASE || 'https://api.classroomcopilot.ai'; await fetch(`${apiBaseUrl}/transcribe/keywords/${watchId}`, { method: 'DELETE', - headers: { 'Authorization': `Bearer ${session.access_token}` }, + headers: { 'Authorization': `Bearer ${token}` }, }); set((state) => ({ keywordWatches: state.keywordWatches.filter((w) => w.id !== watchId) })); } catch (error) { @@ -667,13 +678,13 @@ export const useTranscriptionStore = create((set, get) => ({ if (activeSession) { try { - const { data: { session } } = await supabase.auth.getSession(); + const { _accessToken: _kwToken } = get(); const apiBaseUrl = import.meta.env.VITE_API_BASE || 'https://api.classroomcopilot.ai'; await fetch(`${apiBaseUrl}/transcribe/keywords/events`, { method: 'POST', headers: { 'Content-Type': 'application/json', - ...(session?.access_token ? { 'Authorization': `Bearer ${session.access_token}` } : {}), + ...(_kwToken ? { 'Authorization': `Bearer ${_kwToken}` } : {}), }, body: JSON.stringify({ session_id: activeSession.id, diff --git a/src/utils/tldraw/ui-overrides/components/shared/CCCabinetsPanel.tsx b/src/utils/tldraw/ui-overrides/components/shared/CCCabinetsPanel.tsx index f4406dd..1de467e 100644 --- a/src/utils/tldraw/ui-overrides/components/shared/CCCabinetsPanel.tsx +++ b/src/utils/tldraw/ui-overrides/components/shared/CCCabinetsPanel.tsx @@ -1,17 +1,18 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ThemeProvider, createTheme, useMediaQuery, Box, Grid, Card, CardContent, CardActions, Typography, Button, TextField, Dialog, DialogTitle, DialogContent, DialogActions, IconButton, styled } from '@mui/material'; import EditIcon from '@mui/icons-material/Edit'; import DeleteIcon from '@mui/icons-material/Delete'; import AddIcon from '@mui/icons-material/Add'; import { useTLDraw } from '../../../../../contexts/TLDrawContext'; -import { supabase } from '../../../../../supabaseClient'; +import { useAuth } from '../../../../../contexts/AuthContext'; type Cabinet = { id: string; name: string }; const Toolbar = styled('div')(() => ({ display: 'flex', gap: '8px', marginBottom: '8px' })); export const CCCabinetsPanel: React.FC = () => { - const { tldrawPreferences, authToken } = useTLDraw() as { tldrawPreferences?: { colorScheme?: 'light' | 'dark' | 'system' }, authToken?: string }; + const { tldrawPreferences } = useTLDraw() as { tldrawPreferences?: { colorScheme?: 'light' | 'dark' | 'system' } }; + const { user: authUser, accessToken } = useAuth(); const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); const [cabinets, setCabinets] = useState([]); const [createOpen, setCreateOpen] = useState(false); @@ -28,27 +29,29 @@ export const CCCabinetsPanel: React.FC = () => { const API_BASE: string = import.meta.env.VITE_API_BASE || (location.port.startsWith('517') ? 'http://127.0.0.1:8080' : '/api'); type RequestInitLite = { method?: string; body?: string | FormData | Blob | null; headers?: Record } | undefined; - const apiFetch = async (url: string, init?: RequestInitLite) => { + const apiFetch = useCallback(async (url: string, init?: RequestInitLite) => { const fullUrl = url.startsWith('http') ? url : `${API_BASE}${url}`; - const { data: { session } } = await supabase.auth.getSession(); - const bearer = session?.access_token || authToken || ''; const res = await fetch(fullUrl, { ...init, headers: { - 'Authorization': `Bearer ${bearer}`, + 'Authorization': `Bearer ${accessToken || ''}`, ...(init?.headers || {}) } }); if (!res.ok) throw new Error(await res.text()); return res.json(); - }; + }, [accessToken, API_BASE]); - const loadCabinets = async () => { + const loadCabinets = useCallback(async () => { const data = await apiFetch('/database/cabinets'); setCabinets([...(data.owned || []), ...(data.shared || [])]); - }; + }, [apiFetch]); - useEffect(() => { loadCabinets(); /* eslint-disable-line react-hooks/exhaustive-deps */ }, []); + useEffect(() => { + if (authUser?.id) { + loadCabinets(); + } + }, [loadCabinets, authUser?.id]); const handleCreate = async () => { if (!newName.trim()) return; diff --git a/src/utils/tldraw/ui-overrides/components/shared/CCTranscriptionPanel.tsx b/src/utils/tldraw/ui-overrides/components/shared/CCTranscriptionPanel.tsx index 7ed1215..300b8a7 100644 --- a/src/utils/tldraw/ui-overrides/components/shared/CCTranscriptionPanel.tsx +++ b/src/utils/tldraw/ui-overrides/components/shared/CCTranscriptionPanel.tsx @@ -67,6 +67,7 @@ export const CCTranscriptionPanel: React.FC = () => { flushCanvasEvents, loadSessions, setTimetableContext, + setAuthInfo, llmConfig, summaryText, isGeneratingSummary, @@ -93,7 +94,7 @@ export const CCTranscriptionPanel: React.FC = () => { // Modal state const [showSettingsModal, setShowSettingsModal] = useState(false); - const { user: authUser } = useAuth(); + const { user: authUser, accessToken } = useAuth(); const [showSummaryModal, setShowSummaryModal] = useState(false); const [summaryType, setSummaryType] = useState('full_lesson'); @@ -101,6 +102,11 @@ export const CCTranscriptionPanel: React.FC = () => { const [newKeyword, setNewKeyword] = useState(''); const [isAddingKeyword, setIsAddingKeyword] = useState(false); + // Sync access token into Zustand store so all store actions can use it without getSession() + useEffect(() => { + setAuthInfo(accessToken, authUser?.id ?? null); + }, [accessToken, authUser?.id, setAuthInfo]); + // Load sessions when auth is confirmed (avoids GoTrueClient lock on mount) useEffect(() => { if (authUser?.id) {