- Assist/Agent 模式新增朗读开关 + NSSpeechSynthesizer 语音合成 - 朗读完成后弹窗延迟5秒带动画关闭(不再写死15/20秒) - 打字模式下不自动关闭弹窗 - 二次输入时取消旧朗读/旧延迟关闭,避免竞争问题 - Agent 进度计时器第二次输入时正确归零 - DMG 打包自动清空 models.json API Key - Node.js 运行时打包进 Agent/bin/,用户无需安装 - Agent system prompt 更新为 VoiceInput 身份 + 语音容错说明 - AGENTS.md 全面更新,补全 Agent 模式文档
106 lines
3.0 KiB
Swift
106 lines
3.0 KiB
Swift
import Cocoa
|
||
import SwiftUI
|
||
import DynamicNotchKit
|
||
|
||
/// 管理 AI 回复的刘海弹窗展示
|
||
final class NotchDisplayManager {
|
||
|
||
static let shared = NotchDisplayManager()
|
||
|
||
private var currentNotch: DynamicNotch<NotchContentView>?
|
||
private var isActive = false
|
||
|
||
private init() {}
|
||
|
||
/// 在刘海/浮动区域展示 AI 回复
|
||
/// - Parameter autoHideAfter: 自动隐藏秒数,传 nil 则不自动隐藏
|
||
func show(text: String, autoHideAfter: TimeInterval? = 15) {
|
||
currentNotch?.hide()
|
||
|
||
let content = NotchContentView(
|
||
text: text,
|
||
onCopy: {
|
||
let pasteboard = NSPasteboard.general
|
||
pasteboard.clearContents()
|
||
pasteboard.setString(text, forType: .string)
|
||
}
|
||
)
|
||
|
||
currentNotch = DynamicNotch(style: .notch) {
|
||
content
|
||
}
|
||
isActive = true
|
||
|
||
if let duration = autoHideAfter {
|
||
currentNotch?.show(for: duration)
|
||
} else {
|
||
currentNotch?.show()
|
||
}
|
||
}
|
||
|
||
/// 延长/重置自动隐藏倒计时
|
||
func extendAutoHide(to duration: TimeInterval) {
|
||
currentNotch?.show(for: duration)
|
||
}
|
||
|
||
func hide() {
|
||
currentNotch?.hide()
|
||
currentNotch = nil
|
||
isActive = false
|
||
}
|
||
}
|
||
|
||
// MARK: - 弹窗内容视图
|
||
|
||
struct NotchContentView: View {
|
||
let text: String
|
||
var onCopy: () -> Void
|
||
|
||
private let maxHeight: CGFloat = 220
|
||
|
||
var body: some View {
|
||
VStack(alignment: .leading, spacing: 8) {
|
||
// 标题栏
|
||
HStack(spacing: 0) {
|
||
HStack(spacing: 4) {
|
||
Image(systemName: "sparkles")
|
||
.font(.system(size: 10))
|
||
Text("AI 助手回复")
|
||
.font(.system(size: 11, weight: .medium))
|
||
}
|
||
.foregroundColor(.white.opacity(0.5))
|
||
|
||
Spacer()
|
||
|
||
Button(action: onCopy) {
|
||
HStack(spacing: 3) {
|
||
Image(systemName: "doc.on.doc")
|
||
.font(.system(size: 9))
|
||
Text("复制")
|
||
.font(.system(size: 10))
|
||
}
|
||
.foregroundColor(.white.opacity(0.8))
|
||
.padding(.horizontal, 8)
|
||
.padding(.vertical, 3)
|
||
.background(.white.opacity(0.12))
|
||
.clipShape(Capsule())
|
||
}
|
||
.buttonStyle(.plain)
|
||
}
|
||
|
||
// 文本区域
|
||
ScrollView(.vertical, showsIndicators: false) {
|
||
Text(text)
|
||
.font(.system(size: 13))
|
||
.foregroundColor(.white.opacity(0.85))
|
||
.lineSpacing(4)
|
||
.frame(maxWidth: .infinity, alignment: .leading)
|
||
}
|
||
.frame(maxHeight: maxHeight)
|
||
}
|
||
.padding(.horizontal, 14)
|
||
.padding(.vertical, 10)
|
||
.frame(width: 320)
|
||
}
|
||
}
|