Files
OSGKeyboard/OSGKeyboard/Views/SmartReplyDemoView.swift
T
rocky cfe14f67d4 perf(keyboard): track KeyboardState per property with @Observable
- KeyboardState moves from ObservableObject/@Published to @Observable, so a view re-renders only for the properties it reads. Previously unpublished state and injected action hooks are @ObservationIgnored.
- KeyboardViewController replaces the $phase/$surface Combine sinks with Observations loops tied to the same cancellables.
- AIKeyboardView reads the 200 ms mic level and live ASR partials in small child views (AssistantWaveform, LiveTranscriptReader), so those ticks no longer rebuild the whole assistant surface.
- Tests observe changes with withObservationTracking instead of Combine publishers.
2026-10-21 06:12:21 +02:00

126 lines
4.6 KiB
Swift

// SmartReplyDemoView.swift
// OSGKeyboard · Main App (DEBUG-only)
//
// What's New 2.1.0 recording host for the auto-triggered smart-reply surface.
// Copying a message surfaces the real `replyVariantsSurface` on the unified
// `AIKeyboardView` with Ordinary / Formal / Playful choices. No ASR, no LLM,
// no network — the session is scripted straight into the reply-variant state,
// so the frame is the same view the shipping keyboard shows.
// Launch with `--smart-reply-demo`, optional `--whats-new-lang=zh|en`.
#if DEBUG
import OSGKeyboardShared
import SwiftUI
struct SmartReplyDemoView: View {
@StateObject private var config = ProviderConfig.shared
@State private var state = KeyboardState()
@StateObject private var typing = TypingSessionController()
var body: some View {
VStack(spacing: 0) {
Spacer(minLength: 0)
AIKeyboardView(state: state, typing: typing, onInsert: { _ in })
.background(Palette.light.background)
}
.background(OSGColor.demoBackground.ignoresSafeArea())
.environment(
\.locale,
language == .en ? Locale(identifier: "en") : Locale(identifier: "zh-Hans")
)
.preferredColorScheme(.light)
.task { await runTimeline() }
}
/// Copy → brief thinking → the three personalized reply choices reveal and
/// hold. Mirrors the shipping auto-reply path, which populates the same
/// `aiSession.replyVariants` the coordinator fills from a live request.
private func runTimeline() async {
prepareState()
try? await sleep(1.0)
let utteranceID = UUID()
state.aiSession.beginPreparing(utteranceID: utteranceID)
state.aiSession.beginGenerating(question: "", utteranceID: utteranceID)
try? await sleep(1.1)
state.aiSession.receiveReplyVariants(
Self.variants(language: language),
utteranceID: utteranceID
)
try? await sleep(6.0)
}
private func prepareState() {
config.uiLanguage = language == .en ? .english : .chinese
state.surface = .voice
state.aiServiceAvailable = true
state.micDisabled = false
state.layoutWidth = 390
state.usesIPadLayoutMetrics = false
state.clipboardHistoryEnabled = true
state.clipboardSuggestionText = language == .en
? "Read the proposal — can you also send me the budget version tomorrow?"
: "方案我看了,明天能不能把预算那一版也发我?"
state.aiSession.enter()
}
private static func variants(
language: WhatsNewDemoScenario.Language
) -> [AIReplyVariant] {
if language == .en {
return [
AIReplyVariant(
kind: .ordinary,
emotion: .neutral,
text: "Sure, I'll put the budget version together and send it over tomorrow morning."
),
AIReplyVariant(
kind: .formal,
emotion: .neutral,
text: "Received. I'll finalize the budget version tonight and send it to you tomorrow morning."
),
AIReplyVariant(
kind: .playful,
emotion: .playful,
text: "You got it — the budget sheet is getting dressed up and will be on your desk first thing!"
)
]
}
return [
AIReplyVariant(
kind: .ordinary,
emotion: .neutral,
text: "好的,我整理一下预算版,明天上午发你。"
),
AIReplyVariant(
kind: .formal,
emotion: .neutral,
text: "收到,预算版本我今晚整理完毕,明日上午发送给您。"
),
AIReplyVariant(
kind: .playful,
emotion: .playful,
text: "行嘞~预算表这就去梳妆打扮,明早准时上桌!"
)
]
}
private var language: WhatsNewDemoScenario.Language {
let prefix = "--whats-new-lang="
guard let argument = ProcessInfo.processInfo.arguments.first(
where: { $0.hasPrefix(prefix) }
) else {
return .zh
}
return WhatsNewDemoScenario.Language(
rawValue: String(argument.dropFirst(prefix.count))
) ?? .zh
}
private func sleep(_ seconds: Double) async throws {
try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
}
}
#endif