@@ -14,6 +14,8 @@ struct ContentView: View {
1414 @State private var isRecording = false
1515 @State private var dictation = DictationManager ( )
1616 @State private var dictationHighlightRange : NSRange ? = nil
17+ @State private var dictationCaretPosition : Int ? = nil
18+ @State private var editorCaretPosition : Int = 0
1719 @State private var isDroppingPresentation = false
1820 @State private var dropError : String ?
1921 @State private var dropAlertTitle : String = " Import Error "
@@ -74,30 +76,105 @@ Happy presenting! [wave]
7476 }
7577 }
7678
77- private func startRecording( ) {
78- // Capture the base text once so partial results replace (not append)
79+ @State private var highlightClearTimer : Timer ?
80+
81+ // Segment tracking: each recognition session is a "segment"
82+ @State private var segmentStart : Int = 0
83+ @State private var segmentLength : Int = 0
84+ @State private var segmentNeedsSeparator : Bool = false
85+ // How many chars of the raw recognition result to skip (already committed before cursor move)
86+ @State private var spokenSkipOffset : Int = 0
87+ @State private var lastRawSpokenLength : Int = 0
88+
89+ private func beginNewSegment( ) {
7990 let pageIndex = service. currentPageIndex
80- let baseText = service. pages [ pageIndex]
81- let separator = baseText. isEmpty || baseText. hasSuffix ( " " ) || baseText. hasSuffix ( " \n " ) ? " " : " "
91+ guard pageIndex < service. pages. count else { return }
92+ let text = service. pages [ pageIndex]
93+ let caret = min ( editorCaretPosition, text. count)
94+
95+ // Skip everything already recognized up to this point
96+ spokenSkipOffset = lastRawSpokenLength
97+
98+ // Check if we need a space before the new segment
99+ let charBefore = caret > 0 ? text [ text. index ( text. startIndex, offsetBy: caret - 1 ) ] : " \n "
100+ segmentNeedsSeparator = !( charBefore == " " || charBefore == " \n " || caret == 0 )
101+ segmentStart = caret
102+ segmentLength = 0
103+ }
104+
105+ private func startRecording( ) {
106+ lastRawSpokenLength = 0
107+ spokenSkipOffset = 0
108+ beginNewSegment ( )
109+
110+ dictation. onNewSegment = { [ self ] in
111+ // Recognition restarted — raw counter resets to 0
112+ lastRawSpokenLength = 0
113+ spokenSkipOffset = 0
114+ beginNewSegment ( )
115+ }
82116
83117 dictation. onTextUpdate = { [ self ] spokenText in
118+ lastRawSpokenLength = spokenText. count
119+
120+ // Only use the portion after the skip offset
121+ let effectiveText : String
122+ if spokenSkipOffset < spokenText. count {
123+ effectiveText = String ( spokenText. suffix ( spokenText. count - spokenSkipOffset) )
124+ } else {
125+ effectiveText = " "
126+ }
127+ guard !effectiveText. isEmpty else { return }
128+
129+ let pageIndex = service. currentPageIndex
84130 guard pageIndex < service. pages. count else { return }
85- let newText = baseText + separator + spokenText
86- service. pages [ pageIndex] = newText
87- // Highlight the newly dictated portion
88- let start = baseText. count + separator. count
89- dictationHighlightRange = NSRange ( location: start, length: spokenText. count)
131+ var text = service. pages [ pageIndex]
132+
133+ // Remove the old segment text
134+ let safeStart = min ( segmentStart, text. count)
135+ let removeStart = text. index ( text. startIndex, offsetBy: safeStart)
136+ let safeLen = min ( segmentLength, text. count - safeStart)
137+ let removeEnd = text. index ( removeStart, offsetBy: safeLen)
138+ text. removeSubrange ( removeStart..< removeEnd)
139+
140+ // Build the new segment content
141+ let sep = segmentNeedsSeparator ? " " : " "
142+ let newSegment = sep + effectiveText
143+ text. insert ( contentsOf: newSegment, at: text. index ( text. startIndex, offsetBy: min ( segmentStart, text. count) ) )
144+
145+ let prevLen = segmentLength
146+ segmentLength = newSegment. count
147+ service. pages [ pageIndex] = text
148+
149+ // Highlight only the newly added characters
150+ let newChars = segmentLength - prevLen
151+ if newChars > 0 {
152+ let highlightStart = segmentStart + prevLen
153+ dictationHighlightRange = NSRange ( location: highlightStart, length: newChars)
154+ }
155+
156+ // Move caret to end of segment
157+ dictationCaretPosition = segmentStart + segmentLength
158+
159+ // Clear highlight after 1s of silence
160+ highlightClearTimer? . invalidate ( )
161+ highlightClearTimer = Timer . scheduledTimer ( withTimeInterval: 1.0 , repeats: false ) { _ in
162+ DispatchQueue . main. async {
163+ dictationHighlightRange = nil
164+ }
165+ }
90166 }
91167 dictation. start ( )
92168 isRecording = true
93169 }
94170
95171 private func stopRecording( ) {
96- // Commit: keep whatever was recognized so far
97- let lastText = dictation . audioLevels // just to trigger observation
98- _ = lastText
172+ highlightClearTimer ? . invalidate ( )
173+ highlightClearTimer = nil
174+ dictationHighlightRange = nil
99175 dictation. stop ( )
100176 dictation. onTextUpdate = nil
177+ dictation. onNewSegment = nil
101178 isRecording = false
102179 }
103180
@@ -107,8 +184,18 @@ Happy presenting! [wave]
107184 HighlightingTextEditor (
108185 text: currentText,
109186 font: . systemFont( ofSize: 16 , weight: . regular) . rounded,
110- highlightRange: dictationHighlightRange
187+ highlightRange: dictationHighlightRange,
188+ caretPosition: $dictationCaretPosition,
189+ editorCaretPosition: $editorCaretPosition
111190 )
191+ . onChange ( of: editorCaretPosition) { _, newPos in
192+ guard isRecording else { return }
193+ // If caret moved away from end of current segment, user clicked elsewhere
194+ let segmentEnd = segmentStart + segmentLength
195+ if newPos != segmentEnd {
196+ beginNewSegment ( )
197+ }
198+ }
112199 . padding ( . horizontal, 20 )
113200 . padding ( . top, 8 )
114201 . padding ( . bottom, 8 )
0 commit comments