jeffsi-meet/react/features/meeting-intelligence/actions.ts

496 lines
14 KiB
TypeScript

/**
* Action creators for Meeting Intelligence feature.
*/
import { IReduxState, IStore } from '../app/types';
import {
CLEAR_SEARCH,
CLEAR_SELECTED_MEETING,
EXPORT_FAILURE,
EXPORT_REQUEST,
EXPORT_SUCCESS,
FETCH_MEETINGS_FAILURE,
FETCH_MEETINGS_REQUEST,
FETCH_MEETINGS_SUCCESS,
FETCH_SPEAKER_STATS_SUCCESS,
FETCH_SUMMARY_FAILURE,
FETCH_SUMMARY_REQUEST,
FETCH_SUMMARY_SUCCESS,
FETCH_TRANSCRIPT_FAILURE,
FETCH_TRANSCRIPT_REQUEST,
FETCH_TRANSCRIPT_SUCCESS,
GENERATE_SUMMARY_FAILURE,
GENERATE_SUMMARY_REQUEST,
GENERATE_SUMMARY_SUCCESS,
SEARCH_FAILURE,
SEARCH_REQUEST,
SEARCH_SUCCESS,
SELECT_MEETING,
SET_ACTIVE_TAB,
SET_EXPORT_FORMAT,
SET_SEARCH_QUERY,
TOGGLE_MEETING_INTELLIGENCE,
UPDATE_MEETING_STATUS
} from './actionTypes';
import { API_BASE_URL } from './constants';
import { getAllTokens, getTokenForConference, storeToken } from './tokenStorage';
import {
IMeeting,
IMeetingSummary,
ISearchResult,
ISpeakerStats,
ITranscriptSegment
} from './types';
/**
* Get a Bearer auth header for a meeting, looked up by conference_id from state.
*
* @param {Function} getState - Redux getState function.
* @param {string} meetingId - Optional meeting ID to look up conference_id.
* @returns {Record<string, string>} Headers object with Authorization if token found.
*/
function _getBearerHeaders(getState: () => IReduxState, meetingId?: string): Record<string, string> {
const state = getState();
const miState = state['features/meeting-intelligence'];
let conferenceId: string | undefined;
// Try to find conference_id from selected meeting or provided meetingId
if (miState?.selectedMeeting) {
conferenceId = miState.selectedMeeting.conference_id;
} else if (meetingId && miState?.meetings) {
const meeting = miState.meetings.find((m: IMeeting) => m.id === meetingId);
conferenceId = meeting?.conference_id;
}
// Fall back to current conference room name
if (!conferenceId) {
conferenceId = state['features/base/conference']?.room;
}
if (conferenceId) {
const token = getTokenForConference(conferenceId);
if (token) {
return { Authorization: `Bearer ${token}` };
}
}
return {};
}
/**
* Toggle the meeting intelligence dashboard.
*
* @returns {Object} The action object.
*/
export function toggleMeetingIntelligence() {
return {
type: TOGGLE_MEETING_INTELLIGENCE
};
}
/**
* Set the active tab.
*
* @param {string} tab - The tab to set active.
* @returns {Object} The action object.
*/
export function setActiveTab(tab: 'recordings' | 'transcript' | 'summary' | 'search') {
return {
type: SET_ACTIVE_TAB,
tab
};
}
/**
* Select a meeting for detailed view.
*
* @param {string} meetingId - The meeting ID to select.
* @returns {Function} Async thunk action.
*/
export function selectMeeting(meetingId: string) {
return async (dispatch: IStore['dispatch']) => {
dispatch({ type: SELECT_MEETING, meetingId });
// Fetch transcript and summary for the selected meeting
dispatch(fetchTranscript(meetingId));
dispatch(fetchSummary(meetingId));
dispatch(fetchSpeakerStats(meetingId));
};
}
/**
* Clear the selected meeting.
*
* @returns {Object} The action object.
*/
export function clearSelectedMeeting() {
return {
type: CLEAR_SELECTED_MEETING
};
}
/**
* Fetch access tokens for a conference_id and store them locally.
*
* @param {string} conferenceId - The room name.
* @returns {Function} Async thunk action.
*/
export function fetchMeetingToken(conferenceId: string) {
return async (_dispatch: IStore['dispatch']) => {
try {
const response = await fetch(
`${API_BASE_URL}/meetings/token?conference_id=${encodeURIComponent(conferenceId)}`
);
if (!response.ok) {
return;
}
const data = await response.json();
// Store each token keyed by conference_id
if (Array.isArray(data)) {
for (const entry of data) {
storeToken(entry.conference_id, entry.access_token);
}
}
} catch {
// Token fetch failures are non-fatal
}
};
}
/**
* Fetch the list of meetings.
*
* @returns {Function} Async thunk action.
*/
export function fetchMeetings() {
return async (dispatch: IStore['dispatch']) => {
dispatch({ type: FETCH_MEETINGS_REQUEST });
try {
const tokens = getAllTokens();
const headers: Record<string, string> = {};
if (tokens.length > 0) {
headers['X-MI-Tokens'] = tokens.join(',');
}
const response = await fetch(`${API_BASE_URL}/meetings`, { headers });
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
dispatch({
type: FETCH_MEETINGS_SUCCESS,
meetings: data.meetings as IMeeting[]
});
} catch (error) {
dispatch({
type: FETCH_MEETINGS_FAILURE,
error: error instanceof Error ? error.message : 'Failed to fetch meetings'
});
}
};
}
/**
* Fetch transcript for a meeting.
*
* @param {string} meetingId - The meeting ID.
* @returns {Function} Async thunk action.
*/
export function fetchTranscript(meetingId: string) {
return async (dispatch: IStore['dispatch'], getState: () => IReduxState) => {
dispatch({ type: FETCH_TRANSCRIPT_REQUEST });
try {
const response = await fetch(`${API_BASE_URL}/meetings/${meetingId}/transcript`, {
headers: _getBearerHeaders(getState, meetingId)
});
if (!response.ok) {
if (response.status === 404) {
dispatch({
type: FETCH_TRANSCRIPT_SUCCESS,
transcript: []
});
return;
}
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
dispatch({
type: FETCH_TRANSCRIPT_SUCCESS,
transcript: data.segments as ITranscriptSegment[]
});
} catch (error) {
dispatch({
type: FETCH_TRANSCRIPT_FAILURE,
error: error instanceof Error ? error.message : 'Failed to fetch transcript'
});
}
};
}
/**
* Fetch summary for a meeting.
*
* @param {string} meetingId - The meeting ID.
* @returns {Function} Async thunk action.
*/
export function fetchSummary(meetingId: string) {
return async (dispatch: IStore['dispatch'], getState: () => IReduxState) => {
dispatch({ type: FETCH_SUMMARY_REQUEST });
try {
const response = await fetch(`${API_BASE_URL}/meetings/${meetingId}/summary`, {
headers: _getBearerHeaders(getState, meetingId)
});
if (!response.ok) {
if (response.status === 404) {
dispatch({
type: FETCH_SUMMARY_SUCCESS,
summary: null
});
return;
}
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
dispatch({
type: FETCH_SUMMARY_SUCCESS,
summary: data as IMeetingSummary
});
} catch (error) {
dispatch({
type: FETCH_SUMMARY_FAILURE,
error: error instanceof Error ? error.message : 'Failed to fetch summary'
});
}
};
}
/**
* Generate AI summary for a meeting.
*
* @param {string} meetingId - The meeting ID.
* @returns {Function} Async thunk action.
*/
export function generateSummary(meetingId: string) {
return async (dispatch: IStore['dispatch'], getState: () => IReduxState) => {
dispatch({ type: GENERATE_SUMMARY_REQUEST });
try {
const response = await fetch(`${API_BASE_URL}/meetings/${meetingId}/summary`, {
method: 'POST',
headers: _getBearerHeaders(getState, meetingId)
});
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
dispatch({
type: GENERATE_SUMMARY_SUCCESS,
summary: data as IMeetingSummary
});
} catch (error) {
dispatch({
type: GENERATE_SUMMARY_FAILURE,
error: error instanceof Error ? error.message : 'Failed to generate summary'
});
}
};
}
/**
* Fetch speaker statistics for a meeting.
*
* @param {string} meetingId - The meeting ID.
* @returns {Function} Async thunk action.
*/
export function fetchSpeakerStats(meetingId: string) {
return async (dispatch: IStore['dispatch'], getState: () => IReduxState) => {
try {
const response = await fetch(`${API_BASE_URL}/meetings/${meetingId}/speakers`, {
headers: _getBearerHeaders(getState, meetingId)
});
if (!response.ok) {
return;
}
const data = await response.json();
dispatch({
type: FETCH_SPEAKER_STATS_SUCCESS,
speakerStats: data.speakers as ISpeakerStats[]
});
} catch {
// Silently ignore speaker stats errors
}
};
}
/**
* Search transcripts.
*
* @param {string} query - The search query.
* @returns {Function} Async thunk action.
*/
export function searchTranscripts(query: string) {
return async (dispatch: IStore['dispatch']) => {
dispatch({ type: SET_SEARCH_QUERY, query });
if (!query.trim()) {
dispatch({ type: CLEAR_SEARCH });
return;
}
dispatch({ type: SEARCH_REQUEST });
try {
const tokens = getAllTokens();
const headers: Record<string, string> = {
'Content-Type': 'application/json'
};
if (tokens.length > 0) {
headers['X-MI-Tokens'] = tokens.join(',');
}
const response = await fetch(`${API_BASE_URL}/search`, {
method: 'POST',
headers,
body: JSON.stringify({ query, limit: 50 })
});
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
dispatch({
type: SEARCH_SUCCESS,
results: data.results as ISearchResult[]
});
} catch (error) {
dispatch({
type: SEARCH_FAILURE,
error: error instanceof Error ? error.message : 'Search failed'
});
}
};
}
/**
* Clear search results.
*
* @returns {Object} The action object.
*/
export function clearSearch() {
return { type: CLEAR_SEARCH };
}
/**
* Set export format.
*
* @param {string} format - The export format.
* @returns {Object} The action object.
*/
export function setExportFormat(format: 'pdf' | 'markdown' | 'json') {
return {
type: SET_EXPORT_FORMAT,
format
};
}
/**
* Export a meeting.
*
* @param {string} meetingId - The meeting ID.
* @param {string} format - The export format.
* @returns {Function} Async thunk action.
*/
export function exportMeeting(meetingId: string, format: string) {
return async (dispatch: IStore['dispatch'], getState: () => IReduxState) => {
dispatch({ type: EXPORT_REQUEST });
try {
const response = await fetch(
`${API_BASE_URL}/meetings/${meetingId}/export?format=${format}`,
{ headers: _getBearerHeaders(getState, meetingId) }
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
// Get filename from content-disposition header
const contentDisposition = response.headers.get('content-disposition');
let filename = `meeting-${meetingId}.${format === 'markdown' ? 'md' : format}`;
if (contentDisposition) {
const match = contentDisposition.match(/filename="(.+)"/);
if (match) {
filename = match[1];
}
}
// Download the file
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
dispatch({ type: EXPORT_SUCCESS });
} catch (error) {
dispatch({
type: EXPORT_FAILURE,
error: error instanceof Error ? error.message : 'Export failed'
});
}
};
}
/**
* Update meeting status (from polling).
*
* @param {string} meetingId - The meeting ID.
* @param {string} status - The new status.
* @returns {Object} The action object.
*/
export function updateMeetingStatus(meetingId: string, status: string) {
return {
type: UPDATE_MEETING_STATUS,
meetingId,
status
};
}