

























@@ -251,67 +251,67 @@ struct VoiceWakeSettings: View {
251251252252private var triggerTable: some View {
253253SettingsCardGroup("Trigger Words") {
254-HStack {
255-Text("Wake phrases")
256-.font(.callout.weight(.semibold))
257-Spacer()
258-Button {
259-self.addWord()
260-} label: {
261-Label("Add word", systemImage: "plus")
262-}
263-.disabled(self.triggerEntries
264-.contains(where: { $0.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }))
254+SettingsCardRow(
255+ title: "Wake phrases",
256+ subtitle: "Short phrases that start voice wake detection.")
257+{
258+HStack(spacing: 8) {
259+Button {
260+self.addWord()
261+} label: {
262+Label("Add word", systemImage: "plus")
263+}
264+.disabled(self.triggerEntries
265+.contains(where: { $0.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }))
265266266-Button("Reset defaults") {
267-self.triggerEntries = defaultVoiceWakeTriggers.map { TriggerEntry(id: UUID(), value: $0) }
268-self.syncTriggerEntriesToState()
267+Button("Reset") {
268+self.triggerEntries = defaultVoiceWakeTriggers.map { TriggerEntry(id: UUID(), value: $0) }
269+self.syncTriggerEntriesToState()
270+}
269271}
272+.buttonStyle(.bordered)
270273}
271-.padding(.horizontal, 14)
272-.padding(.top, 12)
273-274-VStack(spacing: 0) {
275-ForEach(self.$triggerEntries) { $entry in
276-HStack(spacing: 8) {
277-TextField("Wake word", text: $entry.value)
278-.textFieldStyle(.roundedBorder)
279-.onSubmit {
280-self.syncTriggerEntriesToState()
281-}
282274283-Button {
284-self.removeWord(id: entry.id)
285-} label: {
286-Image(systemName: "trash")
287-}
288-.buttonStyle(.borderless)
289-.help("Remove trigger word")
290-.frame(width: 24)
291-}
292-.padding(8)
275+self.triggerPhraseRows
293276294-if entry.id != self.triggerEntries.last?.id {
295-Divider()
277+TriggerPhraseHelpRow()
278+}
279+}
280+281+private var triggerPhraseRows: some View {
282+Group {
283+if self.triggerEntries.isEmpty {
284+HStack(spacing: 10) {
285+Image(systemName: "text.badge.plus")
286+.font(.callout)
287+.foregroundStyle(.secondary)
288+.frame(width: 22)
289+Text("No wake phrases configured")
290+.font(.callout.weight(.medium))
291+.foregroundStyle(.secondary)
292+Spacer()
293+}
294+.padding(.horizontal, 14)
295+.padding(.vertical, 14)
296+} else {
297+VStack(spacing: 0) {
298+ForEach(self.$triggerEntries) { $entry in
299+TriggerPhraseRow(
300+ value: $entry.value,
301+ showsDivider: entry.id != self.triggerEntries.last?.id,
302+ onSubmit: {
303+self.syncTriggerEntriesToState()
304+},
305+ onRemove: {
306+self.removeWord(id: entry.id)
307+})
296308}
297309}
298310}
299-.frame(maxWidth: .infinity, minHeight: 180, alignment: .topLeading)
300-.background(Color(nsColor: .textBackgroundColor))
301-.clipShape(RoundedRectangle(cornerRadius: 6))
302-.overlay(
303-RoundedRectangle(cornerRadius: 6)
304-.stroke(Color.secondary.opacity(0.25), lineWidth: 1))
305-.padding(.horizontal, 14)
306-307-Text(
308-"OpenClaw reacts when any trigger appears in a transcription. "
309-+ "Keep them short to avoid false positives.")
310-.font(.footnote)
311-.foregroundStyle(.secondary)
312-.fixedSize(horizontal: false, vertical: true)
313-.padding(.horizontal, 14)
314-.padding(.bottom, 12)
311+}
312+.overlay(alignment: .bottom) {
313+Divider()
314+.padding(.leading, 14)
315315}
316316}
317317@@ -535,70 +535,82 @@ struct VoiceWakeSettings: View {
535535.frame(width: self.controlWidth)
536536}
537537538-if !self.state.voiceWakeAdditionalLocaleIDs.isEmpty {
539-VStack(alignment: .leading, spacing: 8) {
540-Text("Additional languages")
541-.font(.footnote.weight(.semibold))
542-ForEach(
543-Array(self.state.voiceWakeAdditionalLocaleIDs.enumerated()),
544- id: \.offset)
545-{ idx, localeID in
546-HStack(spacing: 8) {
547-Picker("Extra \(idx + 1)", selection: Binding(
548- get: { localeID },
549- set: { newValue in
550-guard self.state
551-.voiceWakeAdditionalLocaleIDs.indices
552-.contains(idx) else { return }
553-self.state
554-.voiceWakeAdditionalLocaleIDs[idx] =
555- newValue
556-})) {
557-ForEach(self.availableLocales.map(\.identifier), id: \.self) { id in
558-Text(self.friendlyName(for: Locale(identifier: id))).tag(id)
559-}
560-}
561-.labelsHidden()
562-.frame(width: 220)
563-564-Button {
565-guard self.state.voiceWakeAdditionalLocaleIDs.indices.contains(idx) else { return }
566-self.state.voiceWakeAdditionalLocaleIDs.remove(at: idx)
567-} label: {
568-Image(systemName: "trash")
569-}
570-.buttonStyle(.borderless)
571-.help("Remove language")
572-}
573-}
574-538+SettingsCardRow(
539+ title: "Additional languages",
540+ subtitle: self.additionalLanguagesSubtitle,
541+ showsDivider: !self.state.voiceWakeAdditionalLocaleIDs.isEmpty)
542+{
543+if self.state.voiceWakeAdditionalLocaleIDs.isEmpty {
575544Button {
576-if let first = availableLocales.first {
577-self.state.voiceWakeAdditionalLocaleIDs.append(first.identifier)
578-}
545+self.addAdditionalLocale()
579546} label: {
580-Label("Add language", systemImage: "plus")
547+Label("Add", systemImage: "plus")
581548}
549+.buttonStyle(.bordered)
582550.disabled(self.availableLocales.isEmpty)
583551}
584-.padding(.horizontal, 14)
585-.padding(.bottom, 10)
586-} else {
552+}
553+554+if !self.state.voiceWakeAdditionalLocaleIDs.isEmpty {
555+self.additionalLanguageRows
556+}
557+}
558+}
559+560+private var additionalLanguagesSubtitle: String {
561+if self.state.voiceWakeAdditionalLocaleIDs.isEmpty {
562+return "None configured."
563+}
564+return "Tried after the primary language."
565+}
566+567+private var additionalLanguageRows: some View {
568+VStack(alignment: .leading, spacing: 0) {
569+ForEach(Array(self.state.voiceWakeAdditionalLocaleIDs.enumerated()), id: \.offset) { idx, localeID in
570+AdditionalLanguageRow(
571+ index: idx,
572+ selection: self.additionalLocaleBinding(index: idx, fallback: localeID),
573+ localeIDs: self.availableLocales.map(\.identifier),
574+ localeName: { id in self.friendlyName(for: Locale(identifier: id)) },
575+ showsDivider: true,
576+ onRemove: {
577+guard self.state.voiceWakeAdditionalLocaleIDs.indices.contains(idx) else { return }
578+self.state.voiceWakeAdditionalLocaleIDs.remove(at: idx)
579+})
580+}
581+582+SettingsCardRow(title: "Add another language", showsDivider: false) {
587583Button {
588-if let first = availableLocales.first {
589-self.state.voiceWakeAdditionalLocaleIDs.append(first.identifier)
590-}
584+self.addAdditionalLocale()
591585} label: {
592-Label("Add additional language", systemImage: "plus")
586+Label("Add", systemImage: "plus")
593587}
594-.buttonStyle(.link)
588+.buttonStyle(.bordered)
595589.disabled(self.availableLocales.isEmpty)
596-.padding(.horizontal, 14)
597-.padding(.bottom, 10)
598590}
599591}
600592}
601593594+private func additionalLocaleBinding(index: Int, fallback: String) -> Binding<String> {
595+Binding(
596+ get: {
597+guard self.state.voiceWakeAdditionalLocaleIDs.indices.contains(index) else { return fallback }
598+return self.state.voiceWakeAdditionalLocaleIDs[index]
599+},
600+ set: { newValue in
601+guard self.state.voiceWakeAdditionalLocaleIDs.indices.contains(index) else { return }
602+self.state.voiceWakeAdditionalLocaleIDs[index] = newValue
603+})
604+}
605+606+private func addAdditionalLocale() {
607+let selected = Set([self.state.voiceWakeLocaleID] + self.state.voiceWakeAdditionalLocaleIDs)
608+let next = self.availableLocales.first { !selected.contains($0.identifier) } ?? self.availableLocales.first
609+if let next {
610+self.state.voiceWakeAdditionalLocaleIDs.append(next.identifier)
611+}
612+}
613+602614@MainActor
603615private func loadMicsIfNeeded(force: Bool = false) async {
604616guard force || self.availableMics.isEmpty, !self.loadingMics else { return }
@@ -716,6 +728,108 @@ struct VoiceWakeSettings: View {
716728}
717729}
718730731+private struct TriggerPhraseRow: View {
732+@Binding var value: String
733+let showsDivider: Bool
734+let onSubmit: () -> Void
735+let onRemove: () -> Void
736+737+var body: some View {
738+HStack(spacing: 12) {
739+Image(systemName: "quote.opening")
740+.font(.callout.weight(.semibold))
741+.foregroundStyle(.secondary)
742+.frame(width: 24)
743+744+TextField("Wake phrase", text: self.$value)
745+.textFieldStyle(.roundedBorder)
746+.font(.callout.weight(.medium))
747+.frame(maxWidth: 420)
748+.onSubmit(self.onSubmit)
749+750+Spacer(minLength: 8)
751+752+Button(action: self.onRemove) {
753+Image(systemName: "trash")
754+.font(.callout)
755+.symbolRenderingMode(.hierarchical)
756+}
757+.buttonStyle(.plain)
758+.foregroundStyle(.secondary)
759+.frame(width: 26, height: 26)
760+.contentShape(Rectangle())
761+.help("Remove trigger word")
762+}
763+.padding(.horizontal, 14)
764+.padding(.vertical, 10)
765+.overlay(alignment: .bottom) {
766+if self.showsDivider {
767+Divider()
768+.padding(.leading, 50)
769+}
770+}
771+}
772+}
773+774+private struct AdditionalLanguageRow: View {
775+let index: Int
776+@Binding var selection: String
777+let localeIDs: [String]
778+let localeName: (String) -> String
779+let showsDivider: Bool
780+let onRemove: () -> Void
781+782+var body: some View {
783+SettingsCardRow(
784+ title: "Language \(self.index + 2)",
785+ subtitle: "Fallback recognition language.",
786+ showsDivider: self.showsDivider)
787+{
788+HStack(spacing: 10) {
789+Picker("Language \(self.index + 2)", selection: self.$selection) {
790+ForEach(self.localeIDs, id: \.self) { id in
791+Text(self.localeName(id)).tag(id)
792+}
793+}
794+.labelsHidden()
795+.frame(width: 220)
796+797+Button(action: self.onRemove) {
798+Image(systemName: "trash")
799+.font(.callout)
800+.symbolRenderingMode(.hierarchical)
801+}
802+.buttonStyle(.plain)
803+.foregroundStyle(.secondary)
804+.frame(width: 26, height: 26)
805+.contentShape(Rectangle())
806+.help("Remove language")
807+}
808+}
809+}
810+}
811+812+private struct TriggerPhraseHelpRow: View {
813+var body: some View {
814+HStack(alignment: .top, spacing: 10) {
815+Image(systemName: "info.circle")
816+.font(.footnote.weight(.semibold))
817+.foregroundStyle(.secondary)
818+.frame(width: 18)
819+.padding(.top, 1)
820+821+Text("OpenClaw reacts when any trigger appears in a transcription. Keep phrases short to avoid false positives.")
822+.font(.footnote)
823+.foregroundStyle(.secondary)
824+.fixedSize(horizontal: false, vertical: true)
825+826+Spacer(minLength: 0)
827+}
828+.padding(.horizontal, 14)
829+.padding(.vertical, 11)
830+}
831+}
832+719833#if DEBUG
720834struct VoiceWakeSettings_Previews: PreviewProvider {
721835static var previews: some View {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。