Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/web/public/locales/translation/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,11 @@ diagram:
desc: XY charts show relationships between two variables
exampleTitle: Sleep vs Work Hours Example
title: XY Chart
dialog:
unsavedChanges:
leave: Leave
message: If you leave this page, your input will be lost. Are you sure you want to leave?
title: Unsaved Changes
drawer:
ai_services: AI Services
builder_mode: Builder Mode
Expand Down
5 changes: 5 additions & 0 deletions packages/web/public/locales/translation/ja.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,11 @@ diagram:
desc: XYチャートは2つの変数間の関係を表現します
exampleTitle: 睡眠・労働時間例
title: XYチャート
dialog:
unsavedChanges:
leave: 移動する
message: このページから移動すると、入力内容が失われます。本当に移動しますか?
title: 入力内容が保存されていません
drawer:
ai_services: AI サービス
builder_mode: ビルダーモード
Expand Down
5 changes: 5 additions & 0 deletions packages/web/public/locales/translation/ko.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,11 @@ diagram:
desc: XY 차트는 두 변수 간의 관계를 보여줍니다
exampleTitle: 수면 vs 근무 시간 예시
title: XY 차트
dialog:
unsavedChanges:
leave: 이동
message: 이 페이지를 떠나면 입력한 내용이 손실됩니다. 정말 이동하시겠습니까?
title: 저장되지 않은 변경사항
drawer:
ai_services: AI 서비스
builder_mode: 빌더 모드
Expand Down
5 changes: 5 additions & 0 deletions packages/web/public/locales/translation/th.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,11 @@ diagram:
desc: XY chart แสดงความสัมพันธ์ระหว่างตัวแปรสองตัว
exampleTitle: ตัวอย่างชั่วโมงนอนเทียบกับชั่วโมงทำงาน
title: XY Chart
dialog:
unsavedChanges:
leave: ออก
message: หากคุณออกจากหน้านี้ ข้อมูลที่ป้อนจะสูญหาย คุณแน่ใจหรือไม่ว่าต้องการออก?
title: การเปลี่ยนแปลงที่ยังไม่ได้บันทึก
drawer:
ai_services: บริการ AI
builder_mode: Builder Mode
Expand Down
5 changes: 5 additions & 0 deletions packages/web/public/locales/translation/vi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,11 @@ diagram:
desc: XY chart thể hiện mối quan hệ giữa hai biến
exampleTitle: Ví dụ thời gian ngủ・làm việc
title: XY Chart
dialog:
unsavedChanges:
leave: Rời khỏi
message: Nếu bạn rời khỏi trang này, nội dung đã nhập sẽ bị mất. Bạn có chắc chắn muốn rời khỏi?
title: Thay đổi chưa được lưu
drawer:
ai_services: Dịch vụ AI
builder_mode: Chế độ builder
Expand Down
5 changes: 5 additions & 0 deletions packages/web/public/locales/translation/zh.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,11 @@ diagram:
desc: XY图表表示两个变量之间的关系
exampleTitle: 睡眠-工作时间示例
title: XY图表
dialog:
unsavedChanges:
leave: 离开
message: 如果离开此页面,您输入的内容将丢失。确定要离开吗?
title: 未保存的更改
drawer:
ai_services: AI 服务
builder_mode: 构建者模式
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,16 @@ interface RealtimeSegment {
interface MeetingMinutesRealtimeTranslationProps {
/** Callback when transcript text changes */
onTranscriptChange?: (text: string) => void;
/** Callback when recording state changes */
onRecordingStateChange?: (state: {
micRecording: boolean;
screenRecording: boolean;
}) => void;
}

const MeetingMinutesRealtimeTranslation: React.FC<
MeetingMinutesRealtimeTranslationProps
> = ({ onTranscriptChange }) => {
> = ({ onTranscriptChange, onRecordingStateChange }) => {
const { t } = useTranslation();
const transcriptContainerRef = useRef<HTMLDivElement>(null);
const isAtBottomRef = useRef<boolean>(true);
Expand All @@ -109,6 +114,14 @@ const MeetingMinutesRealtimeTranslation: React.FC<
rawTranscripts: screenRawTranscripts,
} = useScreenAudio();

// Notify parent component of recording state changes
useEffect(() => {
onRecordingStateChange?.({
micRecording,
screenRecording,
});
}, [micRecording, screenRecording, onRecordingStateChange]);

// Internal state management
const [primaryLanguage, setPrimaryLanguage] = useState('en-US');
const [speakerLabel, setSpeakerLabel] = useState(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,16 @@ interface TranscriptionSegment {
interface MeetingMinutesTranscriptionProps {
/** Callback when transcript text changes */
onTranscriptChange?: (text: string) => void;
/** Callback when recording state changes */
onRecordingStateChange?: (state: {
micRecording: boolean;
screenRecording: boolean;
}) => void;
}

const MeetingMinutesTranscription: React.FC<
MeetingMinutesTranscriptionProps
> = ({ onTranscriptChange }) => {
> = ({ onTranscriptChange, onRecordingStateChange }) => {
const { t, i18n } = useTranslation();
const transcriptContainerRef = useRef<HTMLDivElement>(null);
const isAtBottomRef = useRef<boolean>(true);
Expand All @@ -65,6 +70,14 @@ const MeetingMinutesTranscription: React.FC<
rawTranscripts: screenRawTranscripts,
} = useScreenAudio();

// Notify parent component of recording state changes
useEffect(() => {
onRecordingStateChange?.({
micRecording,
screenRecording,
});
}, [micRecording, screenRecording, onRecordingStateChange]);

// Internal state management
const [languageCode, setLanguageCode] = useState('auto');
const [speakerLabel, setSpeakerLabel] = useState(false);
Expand Down
39 changes: 39 additions & 0 deletions packages/web/src/components/NavigationBlockDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
import ModalDialog from './ModalDialog';
import Button from './Button';
import { useTranslation } from 'react-i18next';

type Props = {
isOpen: boolean;
onCancel: () => void;
onConfirm: () => void;
};

const NavigationBlockDialog: React.FC<Props> = ({
isOpen,
onCancel,
onConfirm,
}) => {
const { t } = useTranslation();

return (
<ModalDialog
isOpen={isOpen}
title={t('dialog.unsavedChanges.title')}
onClose={onCancel}>
<div className="flex flex-col gap-4">
<div>{t('dialog.unsavedChanges.message')}</div>
<div className="flex justify-end gap-2">
<Button onClick={onCancel} className="p-2">
{t('common.cancel')}
</Button>
<Button onClick={onConfirm} className="bg-red-500 p-2">
{t('dialog.unsavedChanges.leave')}
</Button>
</div>
</div>
</ModalDialog>
);
};

export default NavigationBlockDialog;
51 changes: 51 additions & 0 deletions packages/web/src/hooks/usePreventNavigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useEffect } from 'react';
import { useBlocker } from 'react-router-dom';

interface UsePreventNavigationOptions {
/** Block app-internal navigation (sidebar links, back button). Default: true */
blockInternalNavigation?: boolean;
/** Block browser operations (reload, close tab). Default: true */
blockBrowserNavigation?: boolean;
}

/**
* Prevents page navigation when there are unsaved changes
* @param hasUnsavedChanges - Boolean indicating if there are unsaved changes
* @param options - Customization options for blocking behavior
* @returns Blocker object from useBlocker
*/
const usePreventNavigation = (
hasUnsavedChanges: boolean,
options: UsePreventNavigationOptions = {}
) => {
const { blockInternalNavigation = true, blockBrowserNavigation = true } =
options;

// Block app-internal navigation (sidebar links, back button)
const blocker = useBlocker(
({ currentLocation, nextLocation }) =>
blockInternalNavigation &&
hasUnsavedChanges &&
currentLocation.pathname !== nextLocation.pathname
);

// Block browser operations (reload, close tab, navigate to external URL)
useEffect(() => {
if (!blockBrowserNavigation || !hasUnsavedChanges) return;

const handleBeforeUnload = (e: BeforeUnloadEvent) => {
e.preventDefault();
e.returnValue = '';
};

window.addEventListener('beforeunload', handleBeforeUnload);

return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
};
}, [hasUnsavedChanges, blockBrowserNavigation]);

return blocker;
};

export default usePreventNavigation;
45 changes: 44 additions & 1 deletion packages/web/src/pages/MeetingMinutesPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useMemo } from 'react';
import Card from '../components/Card';
import {
PiMicrophoneBold,
Expand All @@ -11,6 +11,8 @@ import MeetingMinutesRealtimeTranslation from '../components/MeetingMinutes/Meet
import MeetingMinutesDirect from '../components/MeetingMinutes/MeetingMinutesDirect';
import MeetingMinutesFile from '../components/MeetingMinutes/MeetingMinutesFile';
import MeetingMinutesGeneration from '../components/MeetingMinutes/MeetingMinutesGeneration';
import NavigationBlockDialog from '../components/NavigationBlockDialog';
import usePreventNavigation from '../hooks/usePreventNavigation';
import { useTranslation } from 'react-i18next';

// Types for Meeting Minutes components
Expand Down Expand Up @@ -59,6 +61,36 @@ const MeetingMinutesPage: React.FC = () => {
realtime_translation: '',
});

// Recording state management for navigation protection
const [transcriptionRecording, setTranscriptionRecording] = useState({
micRecording: false,
screenRecording: false,
});
const [realtimeTranslationRecording, setRealtimeTranslationRecording] =
useState({
micRecording: false,
screenRecording: false,
});

// Check if there are unsaved changes (recording in progress)
const hasUnsavedChanges = useMemo(() => {
if (inputMethod === 'transcription') {
return (
transcriptionRecording.micRecording ||
transcriptionRecording.screenRecording
);
} else if (inputMethod === 'realtime_translation') {
return (
realtimeTranslationRecording.micRecording ||
realtimeTranslationRecording.screenRecording
);
}
return false;
}, [inputMethod, transcriptionRecording, realtimeTranslationRecording]);

// Prevent navigation when recording
const blocker = usePreventNavigation(hasUnsavedChanges);

// Handle transcript changes from components
const handleTranscriptChange = (method: InputMethod, text: string) => {
setTranscriptTexts((prev) => ({
Expand Down Expand Up @@ -101,6 +133,11 @@ const MeetingMinutesPage: React.FC = () => {

return (
<div>
<NavigationBlockDialog
isOpen={blocker.state === 'blocked'}
onCancel={() => blocker.reset?.()}
onConfirm={() => blocker.proceed?.()}
/>
{/* Title Header - Always fixed at top */}
<div className="invisible my-0 flex h-0 items-center justify-center text-xl font-semibold lg:visible lg:my-5 lg:h-min print:visible print:my-5 print:h-min">
{t('meetingMinutes.title')}
Expand Down Expand Up @@ -168,6 +205,9 @@ const MeetingMinutesPage: React.FC = () => {
}}>
<MeetingMinutesTranscription
onTranscriptChange={handleTranscriptionTranscriptChange}
onRecordingStateChange={(state) =>
setTranscriptionRecording(state)
}
/>
</div>
<div
Expand All @@ -177,6 +217,9 @@ const MeetingMinutesPage: React.FC = () => {
}}>
<MeetingMinutesRealtimeTranslation
onTranscriptChange={handleRealtimeTranslationTranscriptChange}
onRecordingStateChange={(state) =>
setRealtimeTranslationRecording(state)
}
/>
</div>
<div
Expand Down
Loading