feat(polish): allow mood emoji on custom styles and ship Flow/ASR fixes

Custom polish styles can opt in to emotion-matched emoji (default off), with
prompt-level opt-in detection so paste-only styles keep model-added emoji.
Also include Volcengine API-Key ASR auth, voice-processing capture, PiP flash
fix, and related keyboard Shift/haptics reliability work.
This commit is contained in:
Rocky
2026-08-06 15:11:12 +08:00
parent 2ed16e1fbf
commit a8d58d8f0c
47 changed files with 1467 additions and 258 deletions
@@ -1,22 +0,0 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="103" y="555" width="81.4876" height="81.4876" rx="40.7438" stroke="white" stroke-opacity="0.8" style="stroke:white;stroke-opacity:0.8;" stroke-width="10"/>
<rect x="103" y="677.633" width="81.4876" height="81.4876" rx="40.7438" stroke="white" stroke-opacity="0.8" style="stroke:white;stroke-opacity:0.8;" stroke-width="10"/>
<rect x="225.632" y="555" width="81.4876" height="81.4876" rx="40.7438" stroke="white" stroke-opacity="0.8" style="stroke:white;stroke-opacity:0.8;" stroke-width="10"/>
<rect x="225.632" y="677.633" width="81.4876" height="81.4876" rx="40.7438" stroke="white" stroke-opacity="0.8" style="stroke:white;stroke-opacity:0.8;" stroke-width="10"/>
<rect x="348.265" y="555" width="81.4876" height="81.4876" rx="40.7438" stroke="white" stroke-opacity="0.8" style="stroke:white;stroke-opacity:0.8;" stroke-width="10"/>
<rect x="348.265" y="677.633" width="81.4876" height="81.4876" rx="40.7438" stroke="white" stroke-opacity="0.8" style="stroke:white;stroke-opacity:0.8;" stroke-width="10"/>
<rect x="225.632" y="827.837" width="572.017" height="81.4876" rx="40.7438" stroke="white" stroke-opacity="0.8" style="stroke:white;stroke-opacity:0.8;" stroke-width="10"/>
<rect x="470.897" y="555" width="81.4876" height="81.4876" rx="40.7438" stroke="white" stroke-opacity="0.8" style="stroke:white;stroke-opacity:0.8;" stroke-width="10"/>
<rect x="470.897" y="677.633" width="81.4876" height="81.4876" rx="40.7438" stroke="white" stroke-opacity="0.8" style="stroke:white;stroke-opacity:0.8;" stroke-width="10"/>
<rect x="716.162" y="555" width="81.4876" height="81.4876" rx="40.7438" stroke="white" stroke-opacity="0.8" style="stroke:white;stroke-opacity:0.8;" stroke-width="10"/>
<rect x="716.162" y="677.633" width="81.4876" height="81.4876" rx="40.7438" stroke="white" stroke-opacity="0.8" style="stroke:white;stroke-opacity:0.8;" stroke-width="10"/>
<rect x="593.529" y="555" width="81.4876" height="81.4876" rx="40.7438" stroke="white" stroke-opacity="0.8" style="stroke:white;stroke-opacity:0.8;" stroke-width="10"/>
<rect x="593.529" y="677.633" width="81.4876" height="81.4876" rx="40.7438" stroke="white" stroke-opacity="0.8" style="stroke:white;stroke-opacity:0.8;" stroke-width="10"/>
<rect x="838.794" y="555" width="81.4876" height="81.4876" rx="40.7438" stroke="white" stroke-opacity="0.8" style="stroke:white;stroke-opacity:0.8;" stroke-width="10"/>
<rect x="838.794" y="677.633" width="81.4876" height="81.4876" rx="40.7438" stroke="white" stroke-opacity="0.8" style="stroke:white;stroke-opacity:0.8;" stroke-width="10"/>
<path d="M578.033 284.581C578.033 248.262 548.706 218.82 512.53 218.82C476.353 218.82 447.026 248.262 447.026 284.581C447.026 320.9 476.353 350.343 512.53 350.343V356.163C473.151 356.163 441.229 324.115 441.229 284.581C441.229 245.048 473.151 213 512.53 213C551.908 213 583.83 245.048 583.83 284.581C583.83 324.115 551.908 356.163 512.53 356.163V350.343C548.706 350.343 578.033 320.9 578.033 284.581Z" fill="#3AA05A" style="fill:#3AA05A;fill:color(display-p3 0.2267 0.6268 0.3534);fill-opacity:1;"/>
<path d="M252 284.581C252 324.115 283.922 356.163 323.301 356.163C362.679 356.163 394.601 324.115 394.601 284.581C394.601 245.048 362.679 213 323.301 213C283.922 213 252 245.048 252 284.581Z" fill="#3AA05A" style="fill:#3AA05A;fill:color(display-p3 0.2267 0.6268 0.3534);fill-opacity:1;"/>
<path d="M701.197 213.561C739.824 213.561 771.275 244.276 772.461 282.615L701.197 282.616V288.229H772.418C770.659 326.044 739.446 356.162 701.197 356.162C661.819 356.162 629.897 324.24 629.896 284.861C629.896 245.483 661.819 213.561 701.197 213.561ZM772.498 284.861C772.498 284.912 772.497 284.964 772.497 285.015V284.707C772.497 284.758 772.498 284.81 772.498 284.861Z" fill="#3AA05A" style="fill:#3AA05A;fill:color(display-p3 0.2267 0.6268 0.3534);fill-opacity:1;"/>
<path d="M447.966 312.933L447.966 307.319L512.53 307.319L512.53 312.933L447.966 312.933Z" fill="#3AA05A" style="fill:#3AA05A;fill:color(display-p3 0.2267 0.6268 0.3534);fill-opacity:1;"/>
<path d="M512.53 268.018V262.404H577.093V268.018H512.53Z" fill="#3AA05A" style="fill:#3AA05A;fill:color(display-p3 0.2267 0.6268 0.3534);fill-opacity:1;"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

@@ -0,0 +1,58 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1024" height="1024" fill="white"/>
<rect width="1024" height="1024" fill="url(#paint0_linear_2178_78)" fill-opacity="0.2"/>
<g filter="url(#filter0_dd_2178_78)">
<path d="M603.944 512.419C603.944 461.795 563.066 420.756 512.64 420.756C462.215 420.756 421.337 461.795 421.337 512.419C421.337 563.043 462.215 604.081 512.64 604.081V612.193C457.752 612.193 413.257 567.523 413.257 512.419C413.257 457.315 457.752 412.644 512.64 412.644C567.528 412.644 612.023 457.315 612.023 512.419C612.023 567.523 567.528 612.193 512.64 612.193V604.081C563.066 604.081 603.944 563.043 603.944 512.419Z" fill="#00AB11"/>
<path d="M603.944 512.419C603.944 461.795 563.066 420.756 512.64 420.756C462.215 420.756 421.337 461.795 421.337 512.419C421.337 563.043 462.215 604.081 512.64 604.081V612.193C457.752 612.193 413.257 567.523 413.257 512.419C413.257 457.315 457.752 412.644 512.64 412.644C567.528 412.644 612.023 457.315 612.023 512.419C612.023 567.523 567.528 612.193 512.64 612.193V604.081C563.066 604.081 603.944 563.043 603.944 512.419Z" fill="url(#paint1_linear_2178_78)" fill-opacity="0.2"/>
<path d="M149.498 512.419C149.498 567.523 193.993 612.193 248.881 612.193C303.769 612.193 348.264 567.523 348.264 512.419C348.264 457.315 303.769 412.644 248.881 412.644C193.993 412.644 149.498 457.315 149.498 512.419Z" fill="#00AB11"/>
<path d="M149.498 512.419C149.498 567.523 193.993 612.193 248.881 612.193C303.769 612.193 348.264 567.523 348.264 512.419C348.264 457.315 303.769 412.644 248.881 412.644C193.993 412.644 149.498 457.315 149.498 512.419Z" fill="url(#paint2_linear_2178_78)" fill-opacity="0.2"/>
<path d="M422.647 551.937L422.647 544.112L512.64 544.112L512.64 551.937L422.647 551.937Z" fill="#00AB11"/>
<path d="M422.647 551.937L422.647 544.112L512.64 544.112L512.64 551.937L422.647 551.937Z" fill="url(#paint3_linear_2178_78)" fill-opacity="0.2"/>
<path d="M512.641 489.332V481.506H602.633V489.332H512.641Z" fill="#00AB11"/>
<path d="M512.641 489.332V481.506H602.633V489.332H512.641Z" fill="url(#paint4_linear_2178_78)" fill-opacity="0.2"/>
<path d="M775.617 413.425C829.458 413.425 873.296 456.239 874.949 509.679H775.617V517.504H874.89C872.438 570.212 828.931 612.191 775.617 612.191C720.729 612.191 676.233 567.696 676.233 512.809C676.233 457.921 720.729 413.425 775.617 413.425ZM875 517.504H874.89C874.962 515.948 875 514.383 875 512.809C875 511.761 874.981 510.718 874.949 509.679H875V517.504Z" fill="#00AB11"/>
<path d="M775.617 413.425C829.458 413.425 873.296 456.239 874.949 509.679H775.617V517.504H874.89C872.438 570.212 828.931 612.191 775.617 612.191C720.729 612.191 676.233 567.696 676.233 512.809C676.233 457.921 720.729 413.425 775.617 413.425ZM875 517.504H874.89C874.962 515.948 875 514.383 875 512.809C875 511.761 874.981 510.718 874.949 509.679H875V517.504Z" fill="url(#paint5_linear_2178_78)" fill-opacity="0.2"/>
</g>
<defs>
<filter id="filter0_dd_2178_78" x="124.498" y="402.644" width="775.502" height="249.549" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.382061 0 0 0 0 0.0382061 0 0 0 0.3 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2178_78"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="15"/>
<feGaussianBlur stdDeviation="12.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.6 0 0 0 0 0.06 0 0 0 0.3 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_2178_78" result="effect2_dropShadow_2178_78"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_2178_78" result="shape"/>
</filter>
<linearGradient id="paint0_linear_2178_78" x1="512" y1="1207" x2="131" y2="56.9999" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-color="#666666" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear_2178_78" x1="443.12" y1="400.5" x2="615.798" y2="422.58" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-color="#666666"/>
</linearGradient>
<linearGradient id="paint2_linear_2178_78" x1="179.361" y1="400.5" x2="352.039" y2="422.58" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-color="#666666"/>
</linearGradient>
<linearGradient id="paint3_linear_2178_78" x1="499.119" y1="552.413" x2="474.127" y2="515.519" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-color="#666666"/>
</linearGradient>
<linearGradient id="paint4_linear_2178_78" x1="589.112" y1="489.808" x2="564.121" y2="452.913" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-color="#666666"/>
</linearGradient>
<linearGradient id="paint5_linear_2178_78" x1="706.097" y1="401.328" x2="878.752" y2="423.493" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-color="#666666"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

+2 -5
View File
@@ -8,11 +8,8 @@
"hidden" : false,
"layers" : [
{
"fill" : "none",
"hidden" : false,
"image-name" : "App Icon Template.svg",
"name" : "App Icon Template",
"opacity" : 1
"image-name" : "Group 29.svg",
"name" : "Group 29"
}
],
"lighting" : "individual",
@@ -787,6 +787,11 @@ final class FlowSessionManager: ObservableObject {
reason = .audioEngineNotLive
} else if !usesPiPKeepAlive, !hasRecentAudio {
reason = .waitingForAudioProof
} else if usesPiPKeepAlive, pipController.isPictureInPictureActive {
// PiP is already up transient gates (!polling / interruption)
// are not a cold start. Prefer awaitingDelivery-adjacent idle over
// `.starting` so the keyboard never flashes.
reason = .awaitingDelivery
} else {
reason = .starting
}
@@ -1331,6 +1336,10 @@ final class FlowSessionManager: ObservableObject {
return
}
FlowSessionBridge.clearResult()
// Ack clears the terminal result; republish ready immediately so the
// keyboard does not linger on awaitingDelivery / starting between the
// 500 ms poll and the next heartbeat.
refreshHostReady()
}
private func hasUnacknowledgedTerminalResult() -> Bool {
+70 -42
View File
@@ -58,41 +58,62 @@ struct ASRSettingsCard: View {
private var volcengineRows: some View {
Group {
SettingsCredentialRow(
title: AppL10n.string("settings.asr.volcengine.appId"),
placeholder: "APP ID",
text: Binding(
get: { volcengineFields.appID },
set: { updateVolcengine(appID: $0) }
),
isSecret: true,
isMonospaced: true
)
Toggle(isOn: volcengineAPIKeyModeBinding) {
VStack(alignment: .leading, spacing: Spacing.xxs) {
Text("settings.asr.volcengine.apiKeyMode.title")
.font(TypeStyle.body)
.foregroundStyle(palette.textPrimary)
Text("settings.asr.volcengine.apiKeyMode.subtitle")
.font(TypeStyle.caption2)
.foregroundStyle(palette.textTertiary)
.fixedSize(horizontal: false, vertical: true)
}
}
.tint(palette.accent)
.settingsListRow()
rowDivider
SettingsCredentialRow(
title: AppL10n.string("settings.asr.volcengine.accessToken"),
placeholder: "Access Token",
text: Binding(
get: { volcengineFields.accessToken },
set: { updateVolcengine(accessToken: $0) }
),
isSecret: true,
isMonospaced: true
)
rowDivider
SettingsCredentialRow(
title: AppL10n.string("settings.asr.volcengine.resourceId"),
placeholder: CloudASRModelCatalog.defaultModel(for: "volcengine"),
text: Binding(
get: { volcengineFields.resourceID },
set: { updateVolcengine(resourceID: $0) }
),
isMonospaced: true,
defaultValue: CloudASRModelCatalog.defaultModel(for: "volcengine")
)
if volcengineFields.usesAPIKeyAuth {
SettingsCredentialRow(
title: AppL10n.string("settings.asr.volcengine.apiKey"),
placeholder: "API Key",
text: Binding(
get: { volcengineFields.apiKeyCredential },
set: { updateVolcengine(apiKeyCredential: $0) }
),
isSecret: true,
isMonospaced: true
)
} else {
SettingsCredentialRow(
title: AppL10n.string("settings.asr.volcengine.appId"),
placeholder: "APP ID",
text: Binding(
get: { volcengineFields.appID },
set: { updateVolcengine(appID: $0) }
),
isSecret: true,
isMonospaced: true
)
rowDivider
SettingsCredentialRow(
title: AppL10n.string("settings.asr.volcengine.accessToken"),
placeholder: "Access Token",
text: Binding(
get: { volcengineFields.accessToken },
set: { updateVolcengine(accessToken: $0) }
),
isSecret: true,
isMonospaced: true
)
}
rowDivider
SettingsProviderRow(title: AppL10n.string("settings.provider.note")) {
Text("settings.asr.volcengine.note")
Text(volcengineFields.usesAPIKeyAuth
? "settings.asr.volcengine.note.apiKey"
: "settings.asr.volcengine.note.appToken")
.font(TypeStyle.caption)
.foregroundStyle(palette.textTertiary)
.fixedSize(horizontal: false, vertical: true)
@@ -105,22 +126,29 @@ struct ASRSettingsCard: View {
}
private var volcengineFields: VolcengineASRFields {
VolcengineASRFields.parse(
apiKey: config.asrApiKey,
resourceFallback: config.asrModel.isEmpty
? CloudASRModelCatalog.defaultModel(for: "volcengine")
: config.asrModel
VolcengineASRFields.parse(apiKey: config.asrApiKey)
}
private var volcengineAPIKeyModeBinding: Binding<Bool> {
Binding(
get: { volcengineFields.usesAPIKeyAuth },
set: { updateVolcengine(authMode: $0 ? .apiKey : .appToken) }
)
}
private func updateVolcengine(appID: String? = nil, accessToken: String? = nil, resourceID: String? = nil) {
private func updateVolcengine(
authMode: VolcengineASRAuthMode? = nil,
appID: String? = nil,
accessToken: String? = nil,
apiKeyCredential: String? = nil
) {
var fields = volcengineFields
if let authMode { fields.authMode = authMode }
if let appID { fields.appID = appID }
if let accessToken { fields.accessToken = accessToken }
if let resourceID {
fields.resourceID = resourceID
config.asrModel = resourceID
}
if let apiKeyCredential { fields.apiKeyCredential = apiKeyCredential }
// Keep store model aligned with the locked SAUC 2.0 resource.
config.asrModel = VolcengineASRFields.fixedResourceID
config.asrApiKey = fields.encodedAPIKey
}
+19 -1
View File
@@ -239,7 +239,8 @@ struct PolishStylesView: View {
format: AppL10n.string("polishStyles.copyName"),
pack.displayName(language: config.uiLanguage)
),
prompt: pack.prompt
prompt: pack.prompt,
allowsAddedEmoji: pack.allowsAddedEmoji
)
showEditor = true
}
@@ -310,12 +311,14 @@ private struct PolishStyleEditorSheet: View {
@Environment(\.themePalette) private var palette
@State private var name: String
@State private var prompt: String
@State private var allowsAddedEmoji: Bool
init(pack: PolishStylePack?, onSave: @escaping (PolishStylePack) -> Void) {
self.pack = pack
self.onSave = onSave
_name = State(initialValue: pack?.name ?? "")
_prompt = State(initialValue: pack?.prompt ?? PolishStylePackCatalog.newUserPromptTemplate)
_allowsAddedEmoji = State(initialValue: pack?.allowsAddedEmoji ?? false)
}
var body: some View {
@@ -324,10 +327,23 @@ private struct PolishStyleEditorSheet: View {
Section("polishStyles.editor.name") {
TextField("polishStyles.editor.namePlaceholder", text: $name)
}
Section {
Toggle("polishStyles.editor.allowsAddedEmoji", isOn: $allowsAddedEmoji)
} footer: {
Text("polishStyles.editor.allowsAddedEmoji.hint")
}
Section {
TextEditor(text: $prompt)
.font(.body.monospaced())
.frame(minHeight: 320)
.onChange(of: prompt) { _, newValue in
// Paste-only custom prompts that declare emoji opt-in
// should flip the toggle so post-processing keeps them.
if !allowsAddedEmoji,
PolishStylePack.promptDeclaresAddedEmojiOptIn(newValue) {
allowsAddedEmoji = true
}
}
} header: {
HStack {
Text("polishStyles.editor.prompt")
@@ -355,6 +371,8 @@ private struct PolishStyleEditorSheet: View {
id: pack?.id ?? "user.\(UUID().uuidString.lowercased())",
name: name,
prompt: prompt,
allowsAddedEmoji: allowsAddedEmoji
|| PolishStylePack.promptDeclaresAddedEmojiOptIn(prompt),
kind: .user,
createdAt: pack?.createdAt ?? Date()
)
+7 -2
View File
@@ -137,8 +137,11 @@
"settings.provider.note" = "Note";
"settings.asr.volcengine.appId" = "APP ID";
"settings.asr.volcengine.accessToken" = "Access Token";
"settings.asr.volcengine.resourceId" = "Resource ID";
"settings.asr.volcengine.note" = "Secret Key is not required. Resource ID defaults to volc.seedasr.sauc.duration.";
"settings.asr.volcengine.apiKey" = "API Key";
"settings.asr.volcengine.apiKeyMode.title" = "Use new API Key auth";
"settings.asr.volcengine.apiKeyMode.subtitle" = "Turn on for the new Volcengine console. Keep off if you already use APP ID + Access Token.";
"settings.asr.volcengine.note.appToken" = "Legacy console: enter APP ID and Access Token. Secret Key is not required. Resource is fixed to Doubao streaming 2.0 (volc.seedasr.sauc.duration).";
"settings.asr.volcengine.note.apiKey" = "New console: enter only the API Key. Resource is fixed to Doubao streaming 2.0 (volc.seedasr.sauc.duration).";
"provider.openai" = "OpenAI";
"provider.deepseek" = "DeepSeek";
"provider.qwen" = "Qwen (DashScope)";
@@ -404,6 +407,8 @@
"polishStyles.copyName" = "%@ Copy";
"polishStyles.editor.name" = "Name";
"polishStyles.editor.namePlaceholder" = "Style name";
"polishStyles.editor.allowsAddedEmoji" = "Allow mood emojis";
"polishStyles.editor.allowsAddedEmoji.hint" = "When on, polish may add a few emojis that match the drafts emotion, and keeps them on screen. Off by default.";
"polishStyles.editor.prompt" = "Complete prompt";
"polishStyles.editor.hint" = "Describe only the role, tone, style boundaries, and examples. The system appends the personal dictionary, safety contract, and output rules.";
"polishStyles.error.title" = "Couldnt save style";
@@ -137,8 +137,11 @@
"settings.provider.note" = "提示";
"settings.asr.volcengine.appId" = "APP ID";
"settings.asr.volcengine.accessToken" = "Access Token";
"settings.asr.volcengine.resourceId" = "Resource ID";
"settings.asr.volcengine.note" = "Secret Key 当前无需填写。Resource ID 默认使用 volc.seedasr.sauc.duration。";
"settings.asr.volcengine.apiKey" = "API Key";
"settings.asr.volcengine.apiKeyMode.title" = "使用新版 API Key 鉴权";
"settings.asr.volcengine.apiKeyMode.subtitle" = "新控制台请打开此开关;已有 AppID + Token 可保持关闭。";
"settings.asr.volcengine.note.appToken" = "旧版控制台:填写 APP ID 与 Access Token。Secret Key 无需填写。识别资源固定为豆包流式 2.0volc.seedasr.sauc.duration)。";
"settings.asr.volcengine.note.apiKey" = "新版控制台:只需填写 API Key。识别资源固定为豆包流式 2.0volc.seedasr.sauc.duration)。";
"provider.openai" = "OpenAI";
"provider.deepseek" = "DeepSeek";
"provider.qwen" = "通义千问";
@@ -403,6 +406,8 @@
"polishStyles.copyName" = "%@副本";
"polishStyles.editor.name" = "名称";
"polishStyles.editor.namePlaceholder" = "风格名称";
"polishStyles.editor.allowsAddedEmoji" = "允许按情绪添加 emoji";
"polishStyles.editor.allowsAddedEmoji.hint" = "开启后,润色可按原文情绪点缀少量 emoji,并保留上屏。默认关闭。";
"polishStyles.editor.prompt" = "完整提示词";
"polishStyles.editor.hint" = "只需描述角色、语气、风格边界和示例。系统会自动追加个人词典、安全边界和输出契约。";
"polishStyles.error.title" = "无法保存风格";