managarten/memoro/apps/mobile/features/memos/contexts/MemoProcessingContext.tsx
Till-JS e7f5f942f3 chore: initial commit - consolidate 4 projects into monorepo
Projects included:
- maerchenzauber (NestJS backend + Expo mobile + SvelteKit web + Astro landing)
- manacore (Expo mobile + SvelteKit web + Astro landing)
- manadeck (NestJS backend + Expo mobile + SvelteKit web)
- memoro (Expo mobile + SvelteKit web + Astro landing)

This commit preserves the current state before monorepo restructuring.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-22 23:38:24 +01:00

294 lines
11 KiB
TypeScript

import React, { createContext, useContext, useRef, useEffect, useState, useCallback } from 'react';
import { useMemoStore } from '../store/memoStore';
import { useAllMemoUpdates } from './MemoRealtimeContext';
import { Memo } from '../types/memo.types';
// Define types for memo processing state
export interface MemoProcessingState {
id: string;
title: string | null;
transcriptionStatus: string | null;
headlineStatus: string | null;
displayTitle: string;
// Track if this is a new memo being processed
isNewUpload: boolean;
}
interface MemoProcessingContextType {
// The current memo being processed
currentMemo: Memo | null;
// The stable processing state that won't cause re-renders
processingState: MemoProcessingState | null;
// A function to get the current display title without re-renders
getDisplayTitle: (memoId: string) => string;
// A function to register a component for updates
registerForUpdates: (callback: () => void) => () => void;
// Force update the title only
forceUpdateTitle: () => void;
}
// Create the context with a default value
const MemoProcessingContext = createContext<MemoProcessingContextType>({
currentMemo: null,
processingState: null,
getDisplayTitle: () => '',
registerForUpdates: () => () => {},
forceUpdateTitle: () => {},
});
// Custom hook to use the context
export const useMemoProcessing = () => useContext(MemoProcessingContext);
export const MemoProcessingProvider: React.FC<{children: React.ReactNode}> = ({ children }) => {
// Reference to the current memo
const currentMemoRef = useRef<any>(null);
// Reference to the processing state (using ref to avoid re-renders)
const processingStateRef = useRef<MemoProcessingState | null>(null);
// Force update counter (used only for the title component)
const [titleUpdateCounter, setTitleUpdateCounter] = useState(0);
// Store callbacks for components that need updates
const callbacksRef = useRef<Set<() => void>>(new Set());
// Register a component for updates
const registerForUpdates = useCallback((callback: () => void) => {
callbacksRef.current.add(callback);
// Return cleanup function
return () => {
callbacksRef.current.delete(callback);
};
}, []);
// Force update only the title component
const forceUpdateTitle = useCallback(() => {
setTitleUpdateCounter(prev => prev + 1);
}, []);
// Get the current display title without causing re-renders
const getDisplayTitle = useCallback((memoId: string): string => {
if (!processingStateRef.current || processingStateRef.current.id !== memoId) {
return '';
}
return processingStateRef.current.displayTitle;
}, []);
// Helper function to calculate display title based on processing state
const calculateDisplayTitle = useCallback((
title: string | null,
transcriptionStatus: string | null,
headlineStatus: string | null
): string => {
console.debug('🔍 calculateDisplayTitle inputs:', { title, transcriptionStatus, headlineStatus });
// HIGHEST PRIORITY: If headline generation is completed AND we have a real title, use it
if (headlineStatus === 'completed' && title && title !== 'Unbenanntes Memo' && title !== 'Neue Aufnahme' && title.trim() !== '') {
console.debug('🌟 Using final headline title:', title);
return title;
}
// If headline is completed but no valid title, this is an error state
if (headlineStatus === 'completed' && (!title || title === 'Unbenanntes Memo' || title === 'Neue Aufnahme' || title.trim() === '')) {
console.debug('⚠️ Headline completed but no valid title, continuing to show generation status');
return 'Headline generating...';
}
// If we have a real title (not default), use it regardless of status
if (title && title !== 'Unbenanntes Memo' && title !== 'Neue Aufnahme') {
console.debug('👍 Using real title:', title);
return title;
}
// Show processing status based on current state
if (transcriptionStatus === 'processing') {
console.debug('📝 Showing transcription status');
return 'Memo transcribing...';
}
if (transcriptionStatus === 'completed' && (!headlineStatus || headlineStatus === 'processing')) {
console.debug('📝 Showing headline generation status');
return 'Headline generating...';
}
// Default
console.debug('📝 Using default title');
return title || 'New Recording';
}, []);
// Update the processing state without causing re-renders
const updateProcessingState = useCallback((newState: Partial<MemoProcessingState>) => {
if (!processingStateRef.current && !newState.id) {
return;
}
console.debug('📝 Updating processing state:', {
id: newState.id || processingStateRef.current?.id,
title: newState.title !== undefined ? newState.title : processingStateRef.current?.title,
transcriptionStatus: newState.transcriptionStatus !== undefined ? newState.transcriptionStatus : processingStateRef.current?.transcriptionStatus,
headlineStatus: newState.headlineStatus !== undefined ? newState.headlineStatus : processingStateRef.current?.headlineStatus,
});
// Calculate the new display title
const newDisplayTitle = calculateDisplayTitle(
newState.title !== undefined ? newState.title : processingStateRef.current?.title || null,
newState.transcriptionStatus !== undefined ? newState.transcriptionStatus : processingStateRef.current?.transcriptionStatus || null,
newState.headlineStatus !== undefined ? newState.headlineStatus : processingStateRef.current?.headlineStatus || null
);
console.debug(`📝 New display title: "${newDisplayTitle}"`);
// Update the ref
processingStateRef.current = {
...(processingStateRef.current || {
id: newState.id as string,
title: null,
transcriptionStatus: null,
headlineStatus: null,
displayTitle: '',
isNewUpload: false,
}),
...newState,
// Always recalculate display title when state changes
displayTitle: newDisplayTitle
} as MemoProcessingState;
// Force update the title component
forceUpdateTitle();
console.debug('📝 Forced title update');
// Notify all registered components
callbacksRef.current.forEach(callback => callback());
console.debug(`📝 Notified ${callbacksRef.current.size} registered components`);
}, [calculateDisplayTitle, forceUpdateTitle]);
// Handle real-time memo updates using centralized service
useAllMemoUpdates((payload) => {
if (payload.event === 'UPDATE') {
console.debug('🔄 REALTIME UPDATE for memo:', payload.new.id);
console.debug('📝 REALTIME UPDATE memo title:', payload.new.title);
console.debug('📝 REALTIME UPDATE transcription status:', payload.new.metadata?.processing?.transcription?.status);
console.debug('📝 REALTIME UPDATE headline status:', payload.new.metadata?.processing?.headline_and_intro?.status);
// Check if this is the current memo
const isCurrentMemo = currentMemoRef.current && currentMemoRef.current.id === payload.new.id;
console.debug(`📝 Is current memo: ${isCurrentMemo ? 'YES' : 'NO'}`);
if (isCurrentMemo) {
// Check if this is a headline completion update
const isHeadlineCompleted =
payload.new.metadata?.processing?.headline_and_intro?.status === 'completed' &&
payload.new.title &&
payload.new.title !== 'Unbenanntes Memo' &&
payload.new.title !== 'Neue Aufnahme';
if (isHeadlineCompleted) {
console.debug('✨ HEADLINE COMPLETED with title:', payload.new.title);
}
// Update the processing state
updateProcessingState({
id: payload.new.id,
title: payload.new.title,
transcriptionStatus: payload.new.metadata?.processing?.transcription?.status,
headlineStatus: payload.new.metadata?.processing?.headline_and_intro?.status,
// Force an immediate title update for headline completion
displayTitle: isHeadlineCompleted ? payload.new.title : undefined,
});
// Update the current memo ref without causing re-renders
currentMemoRef.current = {
...currentMemoRef.current,
title: payload.new.title,
metadata: payload.new.metadata,
};
// For headline completion, force an additional update after a short delay
// This ensures the title update is applied even if there are race conditions
if (isHeadlineCompleted) {
setTimeout(() => {
console.debug('⏱️ Forcing delayed update for headline completion');
forceUpdateTitle();
}, 500);
}
}
} else if (payload.event === 'INSERT') {
console.debug('🆕 NEW MEMO CREATED:', payload.new.id);
// Set as current memo if we don't have one yet
if (!currentMemoRef.current) {
currentMemoRef.current = {
id: payload.new.id,
title: payload.new.title,
metadata: payload.new.metadata,
timestamp: new Date(payload.new.created_at || Date.now()),
source: payload.new.source,
};
// Initialize processing state
updateProcessingState({
id: payload.new.id,
title: payload.new.title,
transcriptionStatus: payload.new.metadata?.processing?.transcription?.status,
headlineStatus: payload.new.metadata?.processing?.headline_and_intro?.status,
isNewUpload: true,
});
}
}
}, [updateProcessingState]);
// Listen for changes to the latest memo from the store
useEffect(() => {
// Define the selector function
const selector = (state: { latestMemo: Memo | null }) => state.latestMemo;
// Define the listener function with proper type
const listener = (latestMemo: Memo | null) => {
if (latestMemo && (!currentMemoRef.current || currentMemoRef.current.id !== latestMemo.id)) {
// Update current memo ref
currentMemoRef.current = latestMemo;
// Initialize processing state
updateProcessingState({
id: latestMemo.id,
title: latestMemo.title || null,
transcriptionStatus: latestMemo.metadata?.processing?.transcription?.status || null,
headlineStatus: latestMemo.metadata?.processing?.headline_and_intro?.status || null,
isNewUpload: false,
});
}
};
// Subscribe to store changes
// Using the store's getState and setState to manually track changes
// since the subscribe method might have different signatures
let previousMemo = selector(useMemoStore.getState());
const unsubscribe = useMemoStore.subscribe((state) => {
const currentMemo = selector(state);
if (currentMemo !== previousMemo) {
previousMemo = currentMemo;
listener(currentMemo);
}
});
return unsubscribe;
}, [updateProcessingState]);
// Context value
const value = {
currentMemo: currentMemoRef.current,
processingState: processingStateRef.current,
getDisplayTitle,
registerForUpdates,
forceUpdateTitle,
};
return (
<MemoProcessingContext.Provider value={value}>
{children}
</MemoProcessingContext.Provider>
);
};