496 lines
14 KiB
TypeScript
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
|
|
};
|
|
}
|