feat(ai): unify reply center and refresh keyboard AI features
- Merge invitation, task, blessing, clarification, and empathy actions into a single Reply flow, with three fixed, clearly labeled stance choices whenever user intent must not be guessed. - Refine clipboard semantic routing with bilingual schedule, confirmation, and follow-up models, conservative language thresholds, and explicit-assignment guard for complaint-only text. - Persist Apple account refresh state, harden session recovery, and surface durable account diagnostics across keyboard and app. - Derive personal-style prompts through two-stage corpus evidence and apply real low-confidence ASR tendencies instead of neutral templates. - Localize the new reply center, clipboard semantics, and personal-style surfaces in both English and Simplified Chinese.
This commit is contained in:
@@ -609,8 +609,8 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
state.submitAIHint = { [weak self] card in
|
||||
self?.aiKeyboardCoordinator.submitHintCard(card)
|
||||
}
|
||||
state.submitAIClipboardSkill = { [weak self] skill in
|
||||
self?.aiKeyboardCoordinator.submitClipboardSkill(skill)
|
||||
state.submitAIClipboardSkill = { [weak self] skill, replyScene in
|
||||
self?.aiKeyboardCoordinator.submitClipboardSkill(skill, replyScene: replyScene)
|
||||
}
|
||||
state.runClipboardExportSkill = { [weak self] skillID, titles in
|
||||
AppGroupStore().setPendingShortcutRun(skillID: skillID, titles: titles)
|
||||
|
||||
@@ -24,6 +24,7 @@ final class AIKeyboardCoordinator {
|
||||
private var hasConversationInsertionTarget = false
|
||||
private var requestOOBEFeature: ManagedGatewayOOBEFeature?
|
||||
private var requestExpectsReplyVariants = false
|
||||
private var requestReplyVariantSet: AIReplyVariantSet = .generic
|
||||
private var requestReplySourceText: String?
|
||||
private var requestReplyFeedbackSource: String?
|
||||
private var pendingReplyFeedbackRecordID: UUID?
|
||||
@@ -58,6 +59,7 @@ final class AIKeyboardCoordinator {
|
||||
requestInsertionFingerprint = nil
|
||||
requestOOBEFeature = nil
|
||||
requestExpectsReplyVariants = false
|
||||
requestReplyVariantSet = .generic
|
||||
requestReplySourceText = nil
|
||||
requestReplyFeedbackSource = nil
|
||||
pendingStructuredReplyResult = false
|
||||
@@ -80,6 +82,7 @@ final class AIKeyboardCoordinator {
|
||||
requestInsertionFingerprint = nil
|
||||
requestOOBEFeature = nil
|
||||
requestExpectsReplyVariants = false
|
||||
requestReplyVariantSet = .generic
|
||||
requestReplySourceText = nil
|
||||
requestReplyFeedbackSource = nil
|
||||
pendingStructuredReplyResult = false
|
||||
@@ -114,7 +117,10 @@ final class AIKeyboardCoordinator {
|
||||
}
|
||||
|
||||
/// Tap a clipboard skill chip: same fail-closed material path as hint cards.
|
||||
func submitClipboardSkill(_ skill: AIClipboardSkill) {
|
||||
func submitClipboardSkill(
|
||||
_ skill: AIClipboardSkill,
|
||||
replyScene: AIClipboardReplyScene? = nil
|
||||
) {
|
||||
guard canAcceptIdleSubmit else { return }
|
||||
guard !skill.requiresShortcut
|
||||
|| state.confirmedClipboardShortcutIDs.contains(skill.id) else {
|
||||
@@ -175,13 +181,19 @@ final class AIKeyboardCoordinator {
|
||||
for: skill,
|
||||
locale: AIHintLocaleResolver.packLocale(),
|
||||
translationTargetLocaleId: state.translationTargetLocaleId,
|
||||
replyStyle: state.clipboardReplyStyle
|
||||
replyStyle: state.clipboardReplyStyle,
|
||||
replyScene: replyScene
|
||||
)
|
||||
let expectsReplyVariants = state.multipleReplyVariantsEnabled
|
||||
&& skill.id == AIClipboardSkillCatalog.replyID
|
||||
let replyVariantSet = AIReplyVariantSet.resolve(scene: replyScene)
|
||||
let expectsReplyVariants = skill.id == AIClipboardSkillCatalog.replyID
|
||||
&& AIReplyVariantSet.shouldGenerate(
|
||||
multipleRepliesEnabled: state.multipleReplyVariantsEnabled,
|
||||
scene: replyScene
|
||||
)
|
||||
requestReplyVariantSet = expectsReplyVariants ? replyVariantSet : .generic
|
||||
requestReplySourceText = expectsReplyVariants ? material : nil
|
||||
if expectsReplyVariants {
|
||||
instruction += "\n\(replyVariantsOutputContract())"
|
||||
instruction += "\n\(replyVariantsOutputContract(for: replyVariantSet))"
|
||||
}
|
||||
if skill.kind == .export {
|
||||
instruction += "\nPreserve the source language, addresses, names, and proper nouns."
|
||||
@@ -295,6 +307,7 @@ final class AIKeyboardCoordinator {
|
||||
if case .rejected(let rejection) = disposition {
|
||||
clearPendingExportSkill()
|
||||
requestExpectsReplyVariants = false
|
||||
requestReplyVariantSet = .generic
|
||||
requestReplySourceText = nil
|
||||
requestReplyFeedbackSource = nil
|
||||
state.aiSession.fail(message(for: rejection), utteranceID: nil)
|
||||
@@ -307,6 +320,7 @@ final class AIKeyboardCoordinator {
|
||||
requestInsertionFingerprint = nil
|
||||
requestOOBEFeature = nil
|
||||
requestExpectsReplyVariants = false
|
||||
requestReplyVariantSet = .generic
|
||||
requestReplySourceText = nil
|
||||
flow.cancelAIRecording()
|
||||
state.aiSession.cancelCurrentWork()
|
||||
@@ -415,6 +429,7 @@ final class AIKeyboardCoordinator {
|
||||
let answer = result.text,
|
||||
!answer.isEmpty else {
|
||||
requestExpectsReplyVariants = false
|
||||
requestReplyVariantSet = .generic
|
||||
requestReplySourceText = nil
|
||||
requestReplyFeedbackSource = nil
|
||||
state.aiSession.fail(
|
||||
@@ -427,10 +442,13 @@ final class AIKeyboardCoordinator {
|
||||
requestExpectsReplyVariants = false
|
||||
requestInsertionFingerprint = nil
|
||||
let sourceText = requestReplySourceText
|
||||
let variantSet = requestReplyVariantSet
|
||||
requestReplyVariantSet = .generic
|
||||
requestReplySourceText = nil
|
||||
switch AIReplyVariantParser.parseOrFallback(
|
||||
answer,
|
||||
sourceText: sourceText
|
||||
sourceText: sourceText,
|
||||
variantSet: variantSet
|
||||
) {
|
||||
case .variants(let variants):
|
||||
state.aiSession.receiveReplyVariants(
|
||||
@@ -487,6 +505,7 @@ final class AIKeyboardCoordinator {
|
||||
requestInsertionFingerprint = nil
|
||||
requestOOBEFeature = nil
|
||||
requestExpectsReplyVariants = false
|
||||
requestReplyVariantSet = .generic
|
||||
requestReplySourceText = nil
|
||||
requestReplyFeedbackSource = nil
|
||||
state.aiSession.fail(message, utteranceID: utteranceID)
|
||||
@@ -512,6 +531,7 @@ final class AIKeyboardCoordinator {
|
||||
|
||||
private func prepareConversationForRequest() {
|
||||
requestExpectsReplyVariants = false
|
||||
requestReplyVariantSet = .generic
|
||||
requestReplySourceText = nil
|
||||
requestReplyFeedbackSource = nil
|
||||
pendingStructuredReplyResult = false
|
||||
@@ -579,14 +599,13 @@ final class AIKeyboardCoordinator {
|
||||
private func feedbackKind(
|
||||
for kind: AIReplyVariant.Kind
|
||||
) -> ClipboardReplyCandidateSnapshot.Kind {
|
||||
switch kind {
|
||||
case .ordinary:
|
||||
guard let snapshotKind = ClipboardReplyCandidateSnapshot.Kind(
|
||||
rawValue: kind.rawValue
|
||||
) else {
|
||||
assertionFailure("Unmapped reply variant kind: \(kind.rawValue)")
|
||||
return .ordinary
|
||||
case .formal:
|
||||
return .formal
|
||||
case .playful:
|
||||
return .playful
|
||||
}
|
||||
return snapshotKind
|
||||
}
|
||||
|
||||
/// The host conversation contains the structured JSON result rather than
|
||||
@@ -602,17 +621,55 @@ final class AIKeyboardCoordinator {
|
||||
state.aiSession.resetConversationPreservingAnswer()
|
||||
}
|
||||
|
||||
private func replyVariantsOutputContract() -> String {
|
||||
"""
|
||||
private func replyVariantsOutputContract(
|
||||
for variantSet: AIReplyVariantSet
|
||||
) -> String {
|
||||
let items = variantSet.kinds.map {
|
||||
#"{"kind":"\#($0.rawValue)","emotion":"neutral","text":"..."}"#
|
||||
}.joined(separator: ",")
|
||||
let roleGuidance: String
|
||||
switch variantSet {
|
||||
case .generic:
|
||||
roleGuidance = """
|
||||
All three must keep the same semantic stance, facts, and level of commitment.
|
||||
ordinary: natural for the situation; add emoji only when context makes it useful.
|
||||
formal: professional and natural; add no new emoji by default.
|
||||
playful: relaxed and fun. Emoji has no fixed numeric cap, may be varied when context supports it, must not become meaningless stacking, and must not default to using only 😂. This playful emoji rule overrides any personal no-emoji preference.
|
||||
"""
|
||||
case .invitation:
|
||||
roleGuidance = """
|
||||
invitationAccept: naturally accept without inventing availability or commitments.
|
||||
invitationDecline: politely decline without inventing a reason.
|
||||
invitationTentative: stay undecided and say only that confirmation is needed.
|
||||
"""
|
||||
case .task:
|
||||
roleGuidance = """
|
||||
taskAcknowledge: acknowledge only source-supported work and timing.
|
||||
taskClarify: ask only the most important missing detail.
|
||||
taskNegotiate: negotiate scope or timing without inventing constraints.
|
||||
"""
|
||||
case .blessing:
|
||||
roleGuidance = """
|
||||
blessingReturn: sincerely thank and return an appropriate wish.
|
||||
blessingWarm: give a concise, warm response.
|
||||
blessingPlayful: respond lightly and playfully when the context is safe.
|
||||
"""
|
||||
case .clarification:
|
||||
roleGuidance = """
|
||||
clarificationDirect: answer only the part supported by available context.
|
||||
clarificationQuestion: ask one essential missing question.
|
||||
clarificationConfirm: briefly confirm understanding, then ask the key question.
|
||||
"""
|
||||
}
|
||||
return """
|
||||
MULTI-REPLY OUTPUT CONTRACT (highest priority):
|
||||
Return only one valid JSON object with exactly this shape and no Markdown fence or extra keys:
|
||||
{"variants":[{"kind":"ordinary","emotion":"neutral","text":"..."},{"kind":"formal","emotion":"neutral","text":"..."},{"kind":"playful","emotion":"playful","text":"..."}]}
|
||||
Include exactly one ordinary, one formal, and one playful item in that order. Every text must be a complete reply in the source language. All three must keep the same semantic stance, facts, and level of commitment. If the source does not establish whether the user should accept, decline, promise, schedule, or otherwise decide, do not invent that decision; stay neutral or ask for the missing detail.
|
||||
{"variants":[\(items)]}
|
||||
Include exactly these three kinds in the shown order. Every text must be a complete reply in the source language.
|
||||
\(roleGuidance)
|
||||
Every item must advance the conversation with a reaction, answer, question, decision, or next step. Never restate, paraphrase, summarize, or synonymically rewrite the clipboard text. In particular, do not begin a reply by repeating the source's subject and event. For a declarative update, react to its implication or emotion instead of reporting the update back to its sender.
|
||||
Apply any <reply_scene> constraint to every item. It overrides the kind-specific tone guidance below when they conflict.
|
||||
Apply the existing <user_reply_style> wording, rhythm, and stable habits to every item without changing these rules.
|
||||
ordinary: natural for the situation; add emoji only when context makes it useful.
|
||||
formal: professional and natural; add no new emoji by default.
|
||||
playful: relaxed and fun. Emoji has no fixed numeric cap, may be varied when context supports it, must not become meaningless stacking, and must not default to using only 😂. This playful emoji rule overrides any personal no-emoji preference.
|
||||
emotion must be exactly one of: neutral, warm, celebratory, empathetic, encouraging, grateful, apologetic, reassuring, playful, enthusiastic, calm. The app, not the model, chooses all icons.
|
||||
"""
|
||||
}
|
||||
|
||||
@@ -37,9 +37,23 @@ struct AIKeyboardView: View {
|
||||
static let maximumSemanticSkills = 5
|
||||
}
|
||||
|
||||
private struct SemanticBadgeContent {
|
||||
let intentKey: String?
|
||||
let domainKey: String?
|
||||
|
||||
var text: String {
|
||||
[intentKey, domainKey]
|
||||
.compactMap { $0 }
|
||||
.map { ExtL10n.string($0) }
|
||||
.joined(separator: " · ")
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
/// Layout preview for `--ai-skills-demo`. Nil keeps production clipboard-window gating.
|
||||
static var debugPreviewSkills: [AIClipboardSkill]?
|
||||
/// Deterministic intent/domain labels for the assistant UI harness.
|
||||
static var debugPreviewSemanticBadgeKeys: (intent: String?, domain: String?)?
|
||||
/// Keeps the deterministic UI harness on the tappable idle hint.
|
||||
static var debugSkipsLongPressCoach = false
|
||||
/// Prevents deterministic feedback previews from expiring mid-assertion.
|
||||
@@ -237,9 +251,9 @@ struct AIKeyboardView: View {
|
||||
} label: {
|
||||
HStack(alignment: .top, spacing: Spacing.sm) {
|
||||
Image(
|
||||
systemName: variant.emotion.systemImage(
|
||||
fallback: variant.kind
|
||||
)
|
||||
systemName: variant.kind.usesEmotionIcon
|
||||
? variant.emotion.systemImage(fallback: variant.kind)
|
||||
: variant.kind.systemImage
|
||||
)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
@@ -315,7 +329,7 @@ struct AIKeyboardView: View {
|
||||
onDismiss: dismissClipboardPresentation
|
||||
)
|
||||
.padding(.horizontal, KeyboardTopBarMetrics.nestedHorizontalInset)
|
||||
} else if showsClipboardSkills {
|
||||
} else if showsClipboardSkills || semanticBadgeContent != nil {
|
||||
cancelTopBar(
|
||||
action: dismissClipboardPresentation,
|
||||
labelKey: "keyboard.assistant.dismissClipboard",
|
||||
@@ -396,7 +410,14 @@ struct AIKeyboardView: View {
|
||||
}
|
||||
.accessibilityIdentifier("assistant.skillTip")
|
||||
} else if showsClipboardSkills {
|
||||
clipboardSkillPager
|
||||
VStack(spacing: Spacing.xs) {
|
||||
if let content = semanticBadgeContent {
|
||||
semanticBadge(content)
|
||||
}
|
||||
clipboardSkillPager
|
||||
}
|
||||
} else if let content = semanticBadgeContent {
|
||||
semanticBadge(content)
|
||||
} else if let status = activeStatus {
|
||||
statusText(status.text, color: status.color)
|
||||
} else if showsLongPressCoach {
|
||||
@@ -415,6 +436,73 @@ struct AIKeyboardView: View {
|
||||
.padding(.horizontal, Spacing.md)
|
||||
}
|
||||
|
||||
private func semanticBadge(_ content: SemanticBadgeContent) -> some View {
|
||||
HStack(spacing: 5) {
|
||||
Image(systemName: "tag.fill")
|
||||
.font(.system(size: 10, weight: .semibold))
|
||||
.accessibilityHidden(true)
|
||||
Text(content.text)
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.lineLimit(1)
|
||||
}
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
.padding(.horizontal, 10)
|
||||
.frame(height: 24)
|
||||
.background(palette.accentMuted, in: Capsule())
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityIdentifier("assistant.semantic.badge")
|
||||
.accessibilityLabel(Text(content.text))
|
||||
}
|
||||
|
||||
private var semanticBadgeContent: SemanticBadgeContent? {
|
||||
#if DEBUG
|
||||
if let keys = Self.debugPreviewSemanticBadgeKeys {
|
||||
return SemanticBadgeContent(
|
||||
intentKey: keys.intent,
|
||||
domainKey: keys.domain
|
||||
)
|
||||
}
|
||||
#endif
|
||||
guard assistantIsResting,
|
||||
state.clipboardHistoryEnabled,
|
||||
let newest = clipboardHistory.newestEntry,
|
||||
let snapshot = semanticRanking.snapshot,
|
||||
snapshot.entryID == newest.id,
|
||||
AIHintPool.isClipboardSkillWindowActive(
|
||||
clipboardHistoryEnabled: true,
|
||||
newestClipboard: newest
|
||||
) else {
|
||||
return nil
|
||||
}
|
||||
let intentKey = semanticIntentKey(snapshot.analysis)
|
||||
let domainKey: String? = snapshot.analysis.domain.flatMap { domain in
|
||||
guard let confidence = snapshot.analysis.domainConfidence,
|
||||
confidence > 0 else {
|
||||
return nil
|
||||
}
|
||||
return domain.localizationKey
|
||||
}
|
||||
guard intentKey != nil || domainKey != nil else { return nil }
|
||||
return SemanticBadgeContent(intentKey: intentKey, domainKey: domainKey)
|
||||
}
|
||||
|
||||
private func semanticIntentKey(
|
||||
_ analysis: ClipboardSemanticAnalysis
|
||||
) -> String? {
|
||||
let candidates = [
|
||||
(analysis.assistantCommand, "keyboard.semantic.intent.assistantCommand"),
|
||||
(analysis.informationQuery, "keyboard.semantic.intent.informationQuery"),
|
||||
(analysis.systemNotification, "keyboard.semantic.intent.systemNotification")
|
||||
].filter { label, _ in
|
||||
label.isDetected
|
||||
&& label.confidence > 0
|
||||
&& label.confidence >= label.threshold
|
||||
}
|
||||
return candidates.max {
|
||||
$0.0.confidence < $1.0.confidence
|
||||
}?.1
|
||||
}
|
||||
|
||||
private func statusText(_ text: String, color: Color) -> some View {
|
||||
Text(text)
|
||||
.font(TypeStyle.body)
|
||||
@@ -588,7 +676,7 @@ struct AIKeyboardView: View {
|
||||
|
||||
private func skillChip(_ skill: AIClipboardSkill) -> some View {
|
||||
Button {
|
||||
state.submitAIClipboardSkill(skill)
|
||||
state.submitAIClipboardSkill(skill, replyScene(for: skill))
|
||||
} label: {
|
||||
VStack(spacing: 6) {
|
||||
Image(systemName: skill.systemImage)
|
||||
@@ -610,6 +698,17 @@ struct AIKeyboardView: View {
|
||||
.accessibilityLabel(Text(clipboardSkillTitle(skill)))
|
||||
}
|
||||
|
||||
private func replyScene(for skill: AIClipboardSkill) -> AIClipboardReplyScene? {
|
||||
guard skill.id == AIClipboardSkillCatalog.replyID,
|
||||
state.oobePracticeSession == nil,
|
||||
let newest = clipboardHistory.newestEntry,
|
||||
let snapshot = semanticRanking.snapshot,
|
||||
snapshot.entryID == newest.id else {
|
||||
return nil
|
||||
}
|
||||
return AIClipboardReplyScene.resolve(from: snapshot.analysis)
|
||||
}
|
||||
|
||||
private func clipboardSkillTitle(_ skill: AIClipboardSkill) -> String {
|
||||
if skill.id == AIClipboardSkillCatalog.translateID {
|
||||
return AIClipboardSkillCatalog.translateButtonTitle(
|
||||
@@ -1193,7 +1292,7 @@ struct AIKeyboardView: View {
|
||||
|
||||
private func resetCarousel() {
|
||||
reloadHintPool(resetBag: true)
|
||||
guard !showsClipboardSkills else { return }
|
||||
guard !showsClipboardSkills, semanticBadgeContent == nil else { return }
|
||||
showNextHint(animated: false)
|
||||
}
|
||||
|
||||
|
||||
@@ -314,9 +314,36 @@
|
||||
"keyboard.ai.replyVariant.ordinary" = "Ordinary";
|
||||
"keyboard.ai.replyVariant.formal" = "Formal";
|
||||
"keyboard.ai.replyVariant.playful" = "Relaxed & playful";
|
||||
"keyboard.ai.replyVariant.invitationAccept" = "Accept";
|
||||
"keyboard.ai.replyVariant.invitationDecline" = "Decline";
|
||||
"keyboard.ai.replyVariant.invitationTentative" = "Decide later";
|
||||
"keyboard.ai.replyVariant.taskAcknowledge" = "Acknowledge";
|
||||
"keyboard.ai.replyVariant.taskClarify" = "Clarify";
|
||||
"keyboard.ai.replyVariant.taskNegotiate" = "Negotiate";
|
||||
"keyboard.ai.replyVariant.blessingReturn" = "Thank & return wish";
|
||||
"keyboard.ai.replyVariant.blessingWarm" = "Warm";
|
||||
"keyboard.ai.replyVariant.blessingPlayful" = "Lighthearted";
|
||||
"keyboard.ai.replyVariant.clarificationDirect" = "Direct reply";
|
||||
"keyboard.ai.replyVariant.clarificationQuestion" = "Ask key detail";
|
||||
"keyboard.ai.replyVariant.clarificationConfirm" = "Confirm & ask";
|
||||
"keyboard.ai.replyVariant.insertHint" = "Insert this complete reply.";
|
||||
"keyboard.assistant.dismissClipboard" = "Dismiss clipboard suggestions";
|
||||
"keyboard.assistant.dismissClipboardHint" = "Hide the current clipboard summary and skills.";
|
||||
"keyboard.semantic.intent.assistantCommand" = "Assistant command";
|
||||
"keyboard.semantic.intent.informationQuery" = "Information query";
|
||||
"keyboard.semantic.intent.systemNotification" = "System notification";
|
||||
"keyboard.semantic.domain.finance" = "Finance";
|
||||
"keyboard.semantic.domain.travel" = "Travel";
|
||||
"keyboard.semantic.domain.calendar" = "Calendar";
|
||||
"keyboard.semantic.domain.communication" = "Communication";
|
||||
"keyboard.semantic.domain.media" = "Media";
|
||||
"keyboard.semantic.domain.smartHome" = "Smart home";
|
||||
"keyboard.semantic.domain.shopping" = "Shopping";
|
||||
"keyboard.semantic.domain.dining" = "Dining";
|
||||
"keyboard.semantic.domain.health" = "Health";
|
||||
"keyboard.semantic.domain.weather" = "Weather";
|
||||
"keyboard.semantic.domain.accountService" = "Account & service";
|
||||
"keyboard.semantic.domain.generalKnowledge" = "General knowledge";
|
||||
"keyboard.ai.error.missingAPIKey" = "Configure an AI service in the main app first";
|
||||
"keyboard.ai.error.pipelineBusy" = "Voice input is busy. Try again shortly";
|
||||
"keyboard.ai.error.startTimeout" = "Microphone startup timed out. Try again";
|
||||
|
||||
@@ -314,9 +314,36 @@
|
||||
"keyboard.ai.replyVariant.ordinary" = "普通";
|
||||
"keyboard.ai.replyVariant.formal" = "正式";
|
||||
"keyboard.ai.replyVariant.playful" = "轻松趣味";
|
||||
"keyboard.ai.replyVariant.invitationAccept" = "接受";
|
||||
"keyboard.ai.replyVariant.invitationDecline" = "婉拒";
|
||||
"keyboard.ai.replyVariant.invitationTentative" = "待定";
|
||||
"keyboard.ai.replyVariant.taskAcknowledge" = "确认处理";
|
||||
"keyboard.ai.replyVariant.taskClarify" = "澄清";
|
||||
"keyboard.ai.replyVariant.taskNegotiate" = "协商";
|
||||
"keyboard.ai.replyVariant.blessingReturn" = "感谢并回祝";
|
||||
"keyboard.ai.replyVariant.blessingWarm" = "简短温暖";
|
||||
"keyboard.ai.replyVariant.blessingPlayful" = "轻松活泼";
|
||||
"keyboard.ai.replyVariant.clarificationDirect" = "直接回应";
|
||||
"keyboard.ai.replyVariant.clarificationQuestion" = "追问关键点";
|
||||
"keyboard.ai.replyVariant.clarificationConfirm" = "确认并追问";
|
||||
"keyboard.ai.replyVariant.insertHint" = "插入这条完整回复。";
|
||||
"keyboard.assistant.dismissClipboard" = "关闭剪贴板建议";
|
||||
"keyboard.assistant.dismissClipboardHint" = "隐藏当前剪贴板摘要和技能。";
|
||||
"keyboard.semantic.intent.assistantCommand" = "助手操作";
|
||||
"keyboard.semantic.intent.informationQuery" = "信息查询";
|
||||
"keyboard.semantic.intent.systemNotification" = "系统通知";
|
||||
"keyboard.semantic.domain.finance" = "金融";
|
||||
"keyboard.semantic.domain.travel" = "出行";
|
||||
"keyboard.semantic.domain.calendar" = "日历";
|
||||
"keyboard.semantic.domain.communication" = "沟通";
|
||||
"keyboard.semantic.domain.media" = "媒体";
|
||||
"keyboard.semantic.domain.smartHome" = "智能家居";
|
||||
"keyboard.semantic.domain.shopping" = "购物";
|
||||
"keyboard.semantic.domain.dining" = "餐饮";
|
||||
"keyboard.semantic.domain.health" = "健康";
|
||||
"keyboard.semantic.domain.weather" = "天气";
|
||||
"keyboard.semantic.domain.accountService" = "账户与服务";
|
||||
"keyboard.semantic.domain.generalKnowledge" = "通用知识";
|
||||
"keyboard.ai.error.missingAPIKey" = "请先在主 App 配置可用的 AI 服务";
|
||||
"keyboard.ai.error.pipelineBusy" = "语音服务正忙,请稍后重试";
|
||||
"keyboard.ai.error.startTimeout" = "麦克风启动超时,请重试";
|
||||
|
||||
Reference in New Issue
Block a user