fix(panels): eliminate getSession() from transcription store and cabinets panel
Same GoTrueClient lock contention fix as CCFilesPanel: - transcriptionStore: add _accessToken/_userId state + setAuthInfo() action; replace all 6 getSession() calls (startSession, flushCanvasEvents, loadSessions, loadKeywordWatches, addKeywordWatch, deleteKeywordWatch, checkSegmentForKeywords) with stored values — zero getSession() calls remain in the store - CCTranscriptionPanel: destructure accessToken from useAuth; sync both values into store via setAuthInfo() on every auth change; gate loadSessions on authUser.id - CCCabinetsPanel: same pattern as CCFilesPanel — useAuth for token, useCallback on apiFetch/loadCabinets, gate effect on authUser.id Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6bd85671d2
commit
3a65cf436b
@ -130,6 +130,11 @@ interface TranscriptionState {
|
|||||||
keywordWatches: KeywordWatch[];
|
keywordWatches: KeywordWatch[];
|
||||||
keywordMatches: KeywordMatch[];
|
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
|
// Actions
|
||||||
startSession: (timetableTag?: TimetablePeriod) => Promise<void>;
|
startSession: (timetableTag?: TimetablePeriod) => Promise<void>;
|
||||||
stopSession: () => Promise<void>;
|
stopSession: () => Promise<void>;
|
||||||
@ -167,6 +172,8 @@ export const useTranscriptionStore = create<TranscriptionState>((set, get) => ({
|
|||||||
isRecording: false,
|
isRecording: false,
|
||||||
isConnecting: false,
|
isConnecting: false,
|
||||||
activeSession: null,
|
activeSession: null,
|
||||||
|
_accessToken: null,
|
||||||
|
_userId: null,
|
||||||
completedSegments: [],
|
completedSegments: [],
|
||||||
serverWindow: [],
|
serverWindow: [],
|
||||||
currentSegment: null,
|
currentSegment: null,
|
||||||
@ -195,19 +202,23 @@ export const useTranscriptionStore = create<TranscriptionState>((set, get) => ({
|
|||||||
set({ timetableContext: context });
|
set({ timetableContext: context });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setAuthInfo: (token: string | null, userId: string | null) => {
|
||||||
|
set({ _accessToken: token, _userId: userId });
|
||||||
|
},
|
||||||
|
|
||||||
startSession: async (timetableTag?: TimetablePeriod) => {
|
startSession: async (timetableTag?: TimetablePeriod) => {
|
||||||
set({ isRecording: true, isConnecting: false, elapsedSeconds: 0, timetableContext: timetableTag || null });
|
set({ isRecording: true, isConnecting: false, elapsedSeconds: 0, timetableContext: timetableTag || null });
|
||||||
|
|
||||||
// Create session in Supabase
|
// Create session in Supabase
|
||||||
try {
|
try {
|
||||||
const { data: sessionData_auth } = await supabase.auth.getSession();
|
const { _accessToken: token, _userId: userId } = get();
|
||||||
if (!sessionData_auth.session?.user) {
|
if (!token || !userId) {
|
||||||
console.error('No authenticated user');
|
console.error('No authenticated user');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionData = {
|
const sessionData = {
|
||||||
user_id: sessionData_auth.session.user.id,
|
user_id: userId,
|
||||||
title: timetableTag?.event_label || 'Untitled Session',
|
title: timetableTag?.event_label || 'Untitled Session',
|
||||||
canvas_type: 'teaching-canvas',
|
canvas_type: 'teaching-canvas',
|
||||||
timetable_period_id: timetableTag?.period_id || null,
|
timetable_period_id: timetableTag?.period_id || null,
|
||||||
@ -465,7 +476,7 @@ export const useTranscriptionStore = create<TranscriptionState>((set, get) => ({
|
|||||||
for (const event of eventsToFlush) {
|
for (const event of eventsToFlush) {
|
||||||
await supabase.from('canvas_events').insert({
|
await supabase.from('canvas_events').insert({
|
||||||
session_id: activeSession?.id || null,
|
session_id: activeSession?.id || null,
|
||||||
user_id: (await supabase.auth.getSession()).data.session?.user?.id || '',
|
user_id: get()._userId || '',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
session_elapsed_seconds: event.sessionElapsedSeconds || null,
|
session_elapsed_seconds: event.sessionElapsedSeconds || null,
|
||||||
event_type: event.eventType,
|
event_type: event.eventType,
|
||||||
@ -484,13 +495,13 @@ export const useTranscriptionStore = create<TranscriptionState>((set, get) => ({
|
|||||||
|
|
||||||
loadSessions: async (): Promise<TranscriptionSession[]> => {
|
loadSessions: async (): Promise<TranscriptionSession[]> => {
|
||||||
try {
|
try {
|
||||||
const { data: sessionData_auth } = await supabase.auth.getSession();
|
const { _userId: userId } = get();
|
||||||
if (!sessionData_auth.session?.user) return [];
|
if (!userId) return [];
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('transcription_sessions')
|
.from('transcription_sessions')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('user_id', sessionData_auth.session.user.id)
|
.eq('user_id', userId)
|
||||||
.order('started_at', { ascending: false })
|
.order('started_at', { ascending: false })
|
||||||
.limit(50);
|
.limit(50);
|
||||||
|
|
||||||
@ -584,11 +595,11 @@ export const useTranscriptionStore = create<TranscriptionState>((set, get) => ({
|
|||||||
|
|
||||||
loadKeywordWatches: async () => {
|
loadKeywordWatches: async () => {
|
||||||
try {
|
try {
|
||||||
const { data: { session } } = await supabase.auth.getSession();
|
const { _accessToken: token } = get();
|
||||||
if (!session?.access_token) return;
|
if (!token) return;
|
||||||
const apiBaseUrl = import.meta.env.VITE_API_BASE || 'https://api.classroomcopilot.ai';
|
const apiBaseUrl = import.meta.env.VITE_API_BASE || 'https://api.classroomcopilot.ai';
|
||||||
const response = await fetch(`${apiBaseUrl}/transcribe/keywords`, {
|
const response = await fetch(`${apiBaseUrl}/transcribe/keywords`, {
|
||||||
headers: { 'Authorization': `Bearer ${session.access_token}` },
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
});
|
});
|
||||||
if (!response.ok) return;
|
if (!response.ok) return;
|
||||||
const watches = await response.json();
|
const watches = await response.json();
|
||||||
@ -600,17 +611,17 @@ export const useTranscriptionStore = create<TranscriptionState>((set, get) => ({
|
|||||||
|
|
||||||
addKeywordWatch: async (keyword: string) => {
|
addKeywordWatch: async (keyword: string) => {
|
||||||
try {
|
try {
|
||||||
const { data: { session } } = await supabase.auth.getSession();
|
const { _accessToken: token, _userId: userId } = get();
|
||||||
if (!session?.access_token || !session.user) return;
|
if (!token || !userId) return;
|
||||||
const apiBaseUrl = import.meta.env.VITE_API_BASE || 'https://api.classroomcopilot.ai';
|
const apiBaseUrl = import.meta.env.VITE_API_BASE || 'https://api.classroomcopilot.ai';
|
||||||
const response = await fetch(`${apiBaseUrl}/transcribe/keywords`, {
|
const response = await fetch(`${apiBaseUrl}/transcribe/keywords`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${session.access_token}`,
|
'Authorization': `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
user_id: session.user.id,
|
user_id: userId,
|
||||||
keyword: keyword.trim(),
|
keyword: keyword.trim(),
|
||||||
match_type: 'contains',
|
match_type: 'contains',
|
||||||
action: 'alert',
|
action: 'alert',
|
||||||
@ -626,12 +637,12 @@ export const useTranscriptionStore = create<TranscriptionState>((set, get) => ({
|
|||||||
|
|
||||||
deleteKeywordWatch: async (watchId: string) => {
|
deleteKeywordWatch: async (watchId: string) => {
|
||||||
try {
|
try {
|
||||||
const { data: { session } } = await supabase.auth.getSession();
|
const { _accessToken: token } = get();
|
||||||
if (!session?.access_token) return;
|
if (!token) return;
|
||||||
const apiBaseUrl = import.meta.env.VITE_API_BASE || 'https://api.classroomcopilot.ai';
|
const apiBaseUrl = import.meta.env.VITE_API_BASE || 'https://api.classroomcopilot.ai';
|
||||||
await fetch(`${apiBaseUrl}/transcribe/keywords/${watchId}`, {
|
await fetch(`${apiBaseUrl}/transcribe/keywords/${watchId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'Authorization': `Bearer ${session.access_token}` },
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
});
|
});
|
||||||
set((state) => ({ keywordWatches: state.keywordWatches.filter((w) => w.id !== watchId) }));
|
set((state) => ({ keywordWatches: state.keywordWatches.filter((w) => w.id !== watchId) }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -667,13 +678,13 @@ export const useTranscriptionStore = create<TranscriptionState>((set, get) => ({
|
|||||||
|
|
||||||
if (activeSession) {
|
if (activeSession) {
|
||||||
try {
|
try {
|
||||||
const { data: { session } } = await supabase.auth.getSession();
|
const { _accessToken: _kwToken } = get();
|
||||||
const apiBaseUrl = import.meta.env.VITE_API_BASE || 'https://api.classroomcopilot.ai';
|
const apiBaseUrl = import.meta.env.VITE_API_BASE || 'https://api.classroomcopilot.ai';
|
||||||
await fetch(`${apiBaseUrl}/transcribe/keywords/events`, {
|
await fetch(`${apiBaseUrl}/transcribe/keywords/events`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...(session?.access_token ? { 'Authorization': `Bearer ${session.access_token}` } : {}),
|
...(_kwToken ? { 'Authorization': `Bearer ${_kwToken}` } : {}),
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
session_id: activeSession.id,
|
session_id: activeSession.id,
|
||||||
|
|||||||
@ -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 { 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 EditIcon from '@mui/icons-material/Edit';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import { useTLDraw } from '../../../../../contexts/TLDrawContext';
|
import { useTLDraw } from '../../../../../contexts/TLDrawContext';
|
||||||
import { supabase } from '../../../../../supabaseClient';
|
import { useAuth } from '../../../../../contexts/AuthContext';
|
||||||
|
|
||||||
type Cabinet = { id: string; name: string };
|
type Cabinet = { id: string; name: string };
|
||||||
|
|
||||||
const Toolbar = styled('div')(() => ({ display: 'flex', gap: '8px', marginBottom: '8px' }));
|
const Toolbar = styled('div')(() => ({ display: 'flex', gap: '8px', marginBottom: '8px' }));
|
||||||
|
|
||||||
export const CCCabinetsPanel: React.FC = () => {
|
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 prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
|
||||||
const [cabinets, setCabinets] = useState<Cabinet[]>([]);
|
const [cabinets, setCabinets] = useState<Cabinet[]>([]);
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
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');
|
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<string, string> } | undefined;
|
type RequestInitLite = { method?: string; body?: string | FormData | Blob | null; headers?: Record<string, string> } | 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 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, {
|
const res = await fetch(fullUrl, {
|
||||||
...init,
|
...init,
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${bearer}`,
|
'Authorization': `Bearer ${accessToken || ''}`,
|
||||||
...(init?.headers || {})
|
...(init?.headers || {})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(await res.text());
|
if (!res.ok) throw new Error(await res.text());
|
||||||
return res.json();
|
return res.json();
|
||||||
};
|
}, [accessToken, API_BASE]);
|
||||||
|
|
||||||
const loadCabinets = async () => {
|
const loadCabinets = useCallback(async () => {
|
||||||
const data = await apiFetch('/database/cabinets');
|
const data = await apiFetch('/database/cabinets');
|
||||||
setCabinets([...(data.owned || []), ...(data.shared || [])]);
|
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 () => {
|
const handleCreate = async () => {
|
||||||
if (!newName.trim()) return;
|
if (!newName.trim()) return;
|
||||||
|
|||||||
@ -67,6 +67,7 @@ export const CCTranscriptionPanel: React.FC = () => {
|
|||||||
flushCanvasEvents,
|
flushCanvasEvents,
|
||||||
loadSessions,
|
loadSessions,
|
||||||
setTimetableContext,
|
setTimetableContext,
|
||||||
|
setAuthInfo,
|
||||||
llmConfig,
|
llmConfig,
|
||||||
summaryText,
|
summaryText,
|
||||||
isGeneratingSummary,
|
isGeneratingSummary,
|
||||||
@ -93,7 +94,7 @@ export const CCTranscriptionPanel: React.FC = () => {
|
|||||||
|
|
||||||
// Modal state
|
// Modal state
|
||||||
const [showSettingsModal, setShowSettingsModal] = useState(false);
|
const [showSettingsModal, setShowSettingsModal] = useState(false);
|
||||||
const { user: authUser } = useAuth();
|
const { user: authUser, accessToken } = useAuth();
|
||||||
const [showSummaryModal, setShowSummaryModal] = useState(false);
|
const [showSummaryModal, setShowSummaryModal] = useState(false);
|
||||||
const [summaryType, setSummaryType] = useState('full_lesson');
|
const [summaryType, setSummaryType] = useState('full_lesson');
|
||||||
|
|
||||||
@ -101,6 +102,11 @@ export const CCTranscriptionPanel: React.FC = () => {
|
|||||||
const [newKeyword, setNewKeyword] = useState('');
|
const [newKeyword, setNewKeyword] = useState('');
|
||||||
const [isAddingKeyword, setIsAddingKeyword] = useState(false);
|
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)
|
// Load sessions when auth is confirmed (avoids GoTrueClient lock on mount)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (authUser?.id) {
|
if (authUser?.id) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user