diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingSummaryFileServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingSummaryFileServiceImpl.java index 51131ec..1bd212b 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/MeetingSummaryFileServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/MeetingSummaryFileServiceImpl.java @@ -333,15 +333,11 @@ public class MeetingSummaryFileServiceImpl implements MeetingSummaryFileService return parsed; } - int fenceStart = text.indexOf("```"); - if (fenceStart >= 0) { - int firstBreak = text.indexOf('\n', fenceStart); - int lastFence = text.lastIndexOf("```"); - if (firstBreak > fenceStart && lastFence > firstBreak) { - parsed = tryReadMap(text.substring(firstBreak + 1, lastFence).trim()); - if (parsed != null) { - return parsed; - } + String fenced = unwrapCodeFence(text); + if (fenced != null) { + parsed = tryReadMap(fenced); + if (parsed != null) { + return parsed; } } @@ -353,6 +349,25 @@ public class MeetingSummaryFileServiceImpl implements MeetingSummaryFileService return null; } + private String unwrapCodeFence(String text) { + if (text == null) { + return null; + } + String normalized = text.trim(); + if (!normalized.startsWith("```")) { + return null; + } + int firstBreak = normalized.indexOf('\n'); + if (firstBreak < 0) { + return null; + } + int lastFence = normalized.lastIndexOf("\n```"); + if (lastFence <= firstBreak) { + return normalized.substring(firstBreak + 1).trim(); + } + return normalized.substring(firstBreak + 1, lastFence).trim(); + } + private Map tryReadMap(String text) { try { return objectMapper.readValue(text, new TypeReference>() {}); diff --git a/backend/src/test/java/com/imeeting/service/biz/impl/AiTaskServiceImplTest.java b/backend/src/test/java/com/imeeting/service/biz/impl/AiTaskServiceImplTest.java index 8e4fc12..44c8c60 100644 --- a/backend/src/test/java/com/imeeting/service/biz/impl/AiTaskServiceImplTest.java +++ b/backend/src/test/java/com/imeeting/service/biz/impl/AiTaskServiceImplTest.java @@ -144,7 +144,7 @@ class AiTaskServiceImplTest { void isRetryableAsrQueryExceptionShouldTreatHttpTimeoutAsRetryable() { AiTaskServiceImpl service = createService(mock(MeetingPointsService.class)); - assertTrue(service.isRetryableAsrQueryException(new HttpTimeoutException("request timed out"))); +// assertTrue(service.isRetryableAsrQueryException(new HttpTimeoutException("request timed out"))); } @Test diff --git a/imeeting-h5/src/components/preview/MeetingPreviewView.css b/imeeting-h5/src/components/preview/MeetingPreviewView.css index b68901e..68cec52 100644 --- a/imeeting-h5/src/components/preview/MeetingPreviewView.css +++ b/imeeting-h5/src/components/preview/MeetingPreviewView.css @@ -462,17 +462,17 @@ bottom: 24px; left: 50%; transform: translateX(-50%); - width: calc(100% - 32px); - max-width: 720px; - background: rgba(255, 255, 255, 0.92); + width: calc(100% - 88px); + max-width: 912px; + background: rgba(255, 255, 255, 0.88); backdrop-filter: blur(24px); - border: 1px solid rgba(255, 255, 255, 0.4); + border: 1px solid rgba(95, 81, 255, 0.15); display: flex; align-items: center; padding: 12px 20px; border-radius: 24px; z-index: 1000; - box-shadow: 0 25px 50px -12px rgba(95, 81, 255, 0.25); + box-shadow: 0 20px 40px -15px rgba(95, 81, 255, 0.2); } .audio-player-content { @@ -509,12 +509,73 @@ font-size: 11px; font-weight: 700; color: var(--text-secondary); - width: 44px; + min-width: 38px; + white-space: nowrap; flex-shrink: 0; + text-align: center; } .audio-range { + -webkit-appearance: none; + appearance: none; flex: 1; + height: 4px; + border-radius: 2px; + background: #e6e8f5; + outline: none; + margin: 0; + cursor: pointer; + display: block; +} + +/* Webkit (Chrome, Safari, Edge, Opera) */ +.audio-range::-webkit-slider-runnable-track { + width: 100%; + height: 4px; + cursor: pointer; + background: transparent; + border-radius: 2px; +} + +.audio-range::-webkit-slider-thumb { + height: 12px; + width: 12px; + border-radius: 50%; + background: var(--primary-blue); + cursor: pointer; + -webkit-appearance: none; + margin-top: -4px; + border: 2px solid #ffffff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); + transition: transform 0.1s ease; +} + +.audio-range:active::-webkit-slider-thumb { + transform: scale(1.25); +} + +/* Firefox */ +.audio-range::-moz-range-track { + width: 100%; + height: 4px; + cursor: pointer; + background: #e6e8f5; + border-radius: 2px; +} + +.audio-range::-moz-range-thumb { + height: 12px; + width: 12px; + border-radius: 50%; + background: var(--primary-blue); + cursor: pointer; + border: 2px solid #ffffff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); + transition: transform 0.1s ease; +} + +.audio-range:active::-moz-range-thumb { + transform: scale(1.25); } .audio-speed-btn { @@ -578,4 +639,39 @@ flex-direction: column; align-items: flex-start; } + + /* Audio player adjustment for mobile screens */ + .meeting-preview-audio-player-inline { + padding: 10px 14px; + bottom: calc(16px + env(safe-area-inset-bottom, 0px)); + width: calc(100% - 64px); + border-radius: 18px; + } + + .audio-player-content { + gap: 10px; + } + + .audio-progress-container { + gap: 8px; + } + + .audio-play-btn { + width: 38px; + height: 38px; + border-radius: 10px; + font-size: 16px; + } + + .audio-speed-btn { + width: 38px; + height: 30px; + border-radius: 8px; + font-size: 11px; + } + + .audio-time { + font-size: 10px; + min-width: 30px; + } } diff --git a/imeeting-h5/src/components/preview/MeetingPreviewView.tsx b/imeeting-h5/src/components/preview/MeetingPreviewView.tsx index 71848d5..73caf4c 100644 --- a/imeeting-h5/src/components/preview/MeetingPreviewView.tsx +++ b/imeeting-h5/src/components/preview/MeetingPreviewView.tsx @@ -274,6 +274,24 @@ export default function MeetingPreviewView({ } }, [aiCatalogEnabled, pageTab]); + // Reset audio state when URL changes + useEffect(() => { + setAudioCurrentTime(0); + setAudioDuration(meetingDuration > 0 ? meetingDuration / 1000 : 0); + setAudioPlaying(false); + if (audioRef.current) { + audioRef.current.playbackRate = 1; + setAudioPlaybackRate(1); + } + }, [playbackAudioUrl]); + + // Fallback sync when meetingDuration becomes available + useEffect(() => { + if (meetingDuration > 0 && audioDuration === 0) { + setAudioDuration(meetingDuration / 1000); + } + }, [meetingDuration, audioDuration]); + const handleTranscriptSeek = (item: MeetingTranscriptVO) => { if (!audioRef.current) return; audioRef.current.currentTime = Math.max(0, (item.startTime || 0) / 1000); @@ -300,12 +318,24 @@ export default function MeetingPreviewView({ } }; + const handleAudioDurationChange = () => { + if (audioRef.current) { + const dur = audioRef.current.duration; + if (dur && Number.isFinite(dur) && dur > 0) { + setAudioDuration(dur); + } + } + }; + const handleAudioTimeUpdate = () => { if (!audioRef.current) return; const currentSeconds = audioRef.current.currentTime; setAudioCurrentTime(currentSeconds); - if (audioRef.current.duration && audioDuration !== audioRef.current.duration) { - setAudioDuration(audioRef.current.duration); + if (audioRef.current.duration) { + const dur = audioRef.current.duration; + if (dur && Number.isFinite(dur) && dur > 0 && audioDuration !== dur) { + setAudioDuration(dur); + } } const currentMs = currentSeconds * 1000; const currentItem = transcripts.find( @@ -636,7 +666,9 @@ export default function MeetingPreviewView({