feat(polish): add context safeguards, layered prompts, and output validation
Use redacted cursor neighborhood and pause-aware chunks for more natural polish, validate protected terms with retry/local fallback, and structure bilingual prompts for consistency and provider prefix caching.
This commit is contained in:
@@ -156,6 +156,7 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
wakeLockView: { [weak self] in self?.view },
|
||||
openHostApp: { [weak self] path in self?.openHostApp(path: path) },
|
||||
detectAndStoreAppContext: { [weak self] in self?.detectAndStoreAppContext() },
|
||||
fieldContextProvider: { [weak self] in self?.captureFieldContext() },
|
||||
scheduleAutoClearError: { [weak self] in self?.scheduleAutoClearError() },
|
||||
refreshConfigFromAppGroup: { [weak self] in self?.configSync.refreshConfigFromAppGroup() }
|
||||
)
|
||||
@@ -315,6 +316,60 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
store.setDetectedAppContext(context)
|
||||
}
|
||||
|
||||
private func captureFieldContext() -> FlowFieldContext {
|
||||
let isSecure = textDocumentProxy.isSecureTextEntry ?? false
|
||||
let preceding = textDocumentProxy.documentContextBeforeInput
|
||||
let following = textDocumentProxy.documentContextAfterInput
|
||||
let isAvailable = preceding != nil || following != nil
|
||||
let isEmpty = isAvailable && (preceding ?? "").isEmpty && (following ?? "").isEmpty
|
||||
|
||||
return FlowFieldContext(
|
||||
precedingText: preceding.map { String($0.suffix(600)) },
|
||||
followingText: following.map { String($0.prefix(200)) },
|
||||
keyboardType: keyboardTypeName(textDocumentProxy.keyboardType ?? .default),
|
||||
returnKeyType: returnKeyTypeName(textDocumentProxy.returnKeyType ?? .default),
|
||||
isSecureEntry: isSecure,
|
||||
isEmptyField: isEmpty,
|
||||
isContextAvailable: isAvailable
|
||||
)
|
||||
}
|
||||
|
||||
private func keyboardTypeName(_ type: UIKeyboardType) -> String {
|
||||
switch type {
|
||||
case .asciiCapable: return "asciiCapable"
|
||||
case .numbersAndPunctuation: return "numbersAndPunctuation"
|
||||
case .URL: return "url"
|
||||
case .numberPad: return "numberPad"
|
||||
case .phonePad: return "phonePad"
|
||||
case .namePhonePad: return "namePhonePad"
|
||||
case .emailAddress: return "emailAddress"
|
||||
case .decimalPad: return "decimalPad"
|
||||
case .twitter: return "twitter"
|
||||
case .webSearch: return "webSearch"
|
||||
case .asciiCapableNumberPad: return "asciiCapableNumberPad"
|
||||
case .default: return "default"
|
||||
@unknown default: return "default"
|
||||
}
|
||||
}
|
||||
|
||||
private func returnKeyTypeName(_ type: UIReturnKeyType) -> String {
|
||||
switch type {
|
||||
case .go: return "go"
|
||||
case .google: return "google"
|
||||
case .join: return "join"
|
||||
case .next: return "next"
|
||||
case .route: return "route"
|
||||
case .search: return "search"
|
||||
case .send: return "send"
|
||||
case .yahoo: return "yahoo"
|
||||
case .done: return "done"
|
||||
case .emergencyCall: return "emergencyCall"
|
||||
case .continue: return "continue"
|
||||
case .default: return "default"
|
||||
@unknown default: return "default"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Open host app
|
||||
|
||||
private func openHostApp(path: String = "settings") {
|
||||
|
||||
@@ -24,6 +24,7 @@ final class KeyboardFlowCoordinator {
|
||||
private let wakeLockView: () -> UIView?
|
||||
private let openHostApp: (String) -> Void
|
||||
private let detectAndStoreAppContext: () -> Void
|
||||
private let fieldContextProvider: () -> FlowFieldContext?
|
||||
private let scheduleAutoClearError: () -> Void
|
||||
private let refreshConfigFromAppGroup: () -> Void
|
||||
|
||||
@@ -71,6 +72,7 @@ final class KeyboardFlowCoordinator {
|
||||
wakeLockView: @escaping () -> UIView?,
|
||||
openHostApp: @escaping (String) -> Void,
|
||||
detectAndStoreAppContext: @escaping () -> Void,
|
||||
fieldContextProvider: @escaping () -> FlowFieldContext?,
|
||||
scheduleAutoClearError: @escaping () -> Void,
|
||||
refreshConfigFromAppGroup: @escaping () -> Void
|
||||
) {
|
||||
@@ -80,6 +82,7 @@ final class KeyboardFlowCoordinator {
|
||||
self.wakeLockView = wakeLockView
|
||||
self.openHostApp = openHostApp
|
||||
self.detectAndStoreAppContext = detectAndStoreAppContext
|
||||
self.fieldContextProvider = fieldContextProvider
|
||||
self.scheduleAutoClearError = scheduleAutoClearError
|
||||
self.refreshConfigFromAppGroup = refreshConfigFromAppGroup
|
||||
}
|
||||
@@ -552,12 +555,15 @@ final class KeyboardFlowCoordinator {
|
||||
utteranceId: currentUtteranceId,
|
||||
commandSeq: nextCommandSeq(),
|
||||
action: action,
|
||||
localeId: state.localeId
|
||||
localeId: state.localeId,
|
||||
fieldContext: action == .stopRecording ? fieldContextProvider() : nil
|
||||
)
|
||||
FlowSessionBridge.writeCommand(command)
|
||||
debug(
|
||||
"command \(action.rawValue) seq=\(command.commandSeq) " +
|
||||
"utterance=\(currentUtteranceId.uuidString)"
|
||||
"utterance=\(currentUtteranceId.uuidString) contextChars=" +
|
||||
"\(command.fieldContext?.precedingText?.count ?? 0)/" +
|
||||
"\(command.fieldContext?.followingText?.count ?? 0)"
|
||||
)
|
||||
// Start of one traceable utterance: everything the host logs afterwards
|
||||
// belongs to this `utterance=` id until the matching keyboard.insert.
|
||||
|
||||
Reference in New Issue
Block a user