Files
OSGKeyboard/OSGKeyboardShared/Models/PolishContext.swift
T
Rocky 31f5937a7f feat(keyboard): improve typing, voice flow, and polish reliability
Reduce extension memory pressure and delivery races while adding richer candidates, tactile feedback, and safer two-level creative polishing.
2026-08-05 21:39:31 +08:00

99 lines
3.5 KiB
Swift

// PolishContext.swift
// OSGKeyboard · Shared
//
// Bag of inputs the LLM polish service needs. Caller assembles it
// before calling `IntelligentPolishingService.polish(_:context:)`.
// Splitting it out keeps the polish service's signature stable as
// we add more signals (app context, personal dictionary, preceding text,
// etc.) over time.
import Foundation
public struct FieldHints: Sendable, Equatable {
public let keyboardType: String?
public let returnKeyType: String?
public let isEmptyField: Bool
public let isContextAvailable: Bool
public init(
keyboardType: String? = nil,
returnKeyType: String? = nil,
isEmptyField: Bool = false,
isContextAvailable: Bool = false
) {
self.keyboardType = keyboardType
self.returnKeyType = returnKeyType
self.isEmptyField = isEmptyField
self.isContextAvailable = isContextAvailable
}
public init(from context: FlowFieldContext) {
self.init(
keyboardType: context.keyboardType,
returnKeyType: context.returnKeyType,
isEmptyField: context.isEmptyField,
isContextAvailable: context.isContextAvailable
)
}
}
public struct PolishContext: Sendable {
/// Coarse classification of the input field. When `.unknown` the
/// LLM is told to pick a neutral tone on its own.
public let appContext: AppContext
/// Optional preceding text (e.g. a few hundred characters of
/// what the user already typed before the recording). The LLM
/// uses it to resolve "this / 那个 / 刚才" references and to
/// bias terminology choices.
public let precedingText: String?
/// Optional text immediately after the insertion point.
public let followingText: String?
/// Input-field signals captured by the keyboard extension.
public let fieldHints: FieldHints?
/// Extra dictionary block appended after `PersonalDictionary.promptFragment()`
/// (e.g. builtin `phrases.tsv` terms on macOS local ASR).
public let dictionarySupplement: String?
/// Cap on how many characters of `precedingText` we actually
/// include in the prompt. The full preceding text is often
/// hundreds of KB in a long note — we only need the tail.
public let maxPrecedingChars: Int
public let maxFollowingChars: Int
public init(
appContext: AppContext = .unknown,
precedingText: String? = nil,
followingText: String? = nil,
fieldHints: FieldHints? = nil,
dictionarySupplement: String? = nil,
maxPrecedingChars: Int = 600,
maxFollowingChars: Int = 200
) {
self.appContext = appContext
self.precedingText = precedingText
self.followingText = followingText
self.fieldHints = fieldHints
self.dictionarySupplement = dictionarySupplement
self.maxPrecedingChars = maxPrecedingChars
self.maxFollowingChars = maxFollowingChars
}
/// Truncated view of `precedingText` ready for prompt injection.
/// Returns `nil` when there is nothing meaningful to add.
public var precedingForPrompt: String? {
guard let raw = precedingText, !raw.isEmpty else { return nil }
if raw.count <= maxPrecedingChars { return raw }
return String(raw.suffix(maxPrecedingChars))
}
public var followingForPrompt: String? {
guard let raw = followingText, !raw.isEmpty else { return nil }
if raw.count <= maxFollowingChars { return raw }
return String(raw.prefix(maxFollowingChars))
}
}