rt-chat-input 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -12
- package/dist/chat-input.es.js +3 -3
- package/dist/chat-input.umd.js +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,18 +79,18 @@ const handleAttach = (files) => {
|
|
|
79
79
|
|
|
80
80
|
#### Props
|
|
81
81
|
|
|
82
|
-
| 属性名 | 类型 | 默认值 | 说明
|
|
83
|
-
| ---------------------- | ----------------------------- | ------------------------- |
|
|
84
|
-
| `placeholder` | `string` | `'请输入,或按住说话...'` | 输入框占位符
|
|
85
|
-
| `disabled` | `boolean` | `false` | 是否禁用
|
|
86
|
-
| `wsUrl` | `string` | `undefined` |
|
|
87
|
-
| `maxVoiceDuration` | `number` | `60` | 最大录音时长(秒)
|
|
88
|
-
| `theme` | `'light' \| 'dark' \| 'auto'` | `'light'` | 主题模式
|
|
89
|
-
| `fixed` | `boolean` | `false` | 是否开启固定底部定位(开箱即用模式)
|
|
90
|
-
| `bottomOffset` | `number \| string` | `36` | 底部距离偏移量(仅 fixed=true 时有效),数字单位 px
|
|
91
|
-
| `showVoiceButton` | `boolean` | `true` | 是否显示语音按钮
|
|
92
|
-
| `showAttachmentButton` | `boolean` | `true` | 是否显示附件按钮
|
|
93
|
-
| `acceptFileTypes` | `string` | `'image/*,.pdf...'`
|
|
82
|
+
| 属性名 | 类型 | 默认值 | 说明 |
|
|
83
|
+
| ---------------------- | ----------------------------- | ------------------------- | -------------------------------------------------------------- |
|
|
84
|
+
| `placeholder` | `string` | `'请输入,或按住说话...'` | 输入框占位符 |
|
|
85
|
+
| `disabled` | `boolean` | `false` | 是否禁用 |
|
|
86
|
+
| `wsUrl` | `string` | `undefined` | 语音转写 WebSocket 地址(见下方[后端接入指南](#后端接入指南)) |
|
|
87
|
+
| `maxVoiceDuration` | `number` | `60` | 最大录音时长(秒) |
|
|
88
|
+
| `theme` | `'light' \| 'dark' \| 'auto'` | `'light'` | 主题模式 |
|
|
89
|
+
| `fixed` | `boolean` | `false` | 是否开启固定底部定位(开箱即用模式) |
|
|
90
|
+
| `bottomOffset` | `number \| string` | `36` | 底部距离偏移量(仅 fixed=true 时有效),数字单位 px |
|
|
91
|
+
| `showVoiceButton` | `boolean` | `true` | 是否显示语音按钮 |
|
|
92
|
+
| `showAttachmentButton` | `boolean` | `true` | 是否显示附件按钮 |
|
|
93
|
+
| `acceptFileTypes` | `string` | `'image/*,.pdf,.doc...'` | 允许上传的文件类型(默认为常见图片及办公文档格式) |
|
|
94
94
|
|
|
95
95
|
#### Events
|
|
96
96
|
|
|
@@ -101,6 +101,9 @@ const handleAttach = (files) => {
|
|
|
101
101
|
| `change` | `(text: string)` | 输入框内容变化时触发 |
|
|
102
102
|
| `attach` | `(files: File[])` | 附件列表变化时触发 |
|
|
103
103
|
| `error` | `(message: string)` | 发生错误时触发 |
|
|
104
|
+
| `stop` | `-` | 点击停止按钮时触发(仅 loading=true 时) |
|
|
105
|
+
| `focus` | `(e: FocusEvent)` | 输入框获得焦点时触发 |
|
|
106
|
+
| `blur` | `(e: FocusEvent)` | 输入框失去焦点时触发 |
|
|
104
107
|
|
|
105
108
|
### VoiceRecorder 组件
|
|
106
109
|
|
|
@@ -116,6 +119,92 @@ import { VoiceRecorder } from "rt-chat-input";
|
|
|
116
119
|
</template>
|
|
117
120
|
```
|
|
118
121
|
|
|
122
|
+
## 后端接入指南
|
|
123
|
+
|
|
124
|
+
组件通过 WebSocket 发送 16k 采样率的 PCM 音频流,并期望接收 JSON 格式的转写结果。
|
|
125
|
+
|
|
126
|
+
### 1. 通信协议
|
|
127
|
+
|
|
128
|
+
- **客户端发送**:Raw PCM Audio (Int16, 16000Hz, Mono)
|
|
129
|
+
- **服务端返回**:JSON 字符串
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"text": "转写文本内容",
|
|
133
|
+
"isFinal": false // true 表示句尾(最终结果),false 表示中间结果
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 2. Spring WebFlux 实现示例
|
|
138
|
+
|
|
139
|
+
推荐使用后端作为代理连接 FunASR 服务(避免前端直接处理复杂的握手协议)。
|
|
140
|
+
|
|
141
|
+
```kotlin
|
|
142
|
+
// WebSocketHandler 实现
|
|
143
|
+
@Component
|
|
144
|
+
class RealTimeTranscriptionHandler(
|
|
145
|
+
private val objectMapper: ObjectMapper
|
|
146
|
+
) : WebSocketHandler {
|
|
147
|
+
|
|
148
|
+
// FunASR 服务地址 (e.g. ws://192.168.1.100:10095)
|
|
149
|
+
@Value("\${asr.service.url}")
|
|
150
|
+
private lateinit var asrServiceUrl: String
|
|
151
|
+
|
|
152
|
+
override fun handle(session: WebSocketSession): Mono<Void> {
|
|
153
|
+
val client = ReactorNettyWebSocketClient()
|
|
154
|
+
return client.execute(URI(asrServiceUrl)) { asrSession ->
|
|
155
|
+
// 1. 发送 FunASR 握手包 (必须配置如下参数以匹配模型要求)
|
|
156
|
+
val handshake = mapOf(
|
|
157
|
+
"mode" to "2pass",
|
|
158
|
+
"chunk_size" to listOf(5, 10, 5),
|
|
159
|
+
"encoder_chunk_look_back" to 4,
|
|
160
|
+
"decoder_chunk_look_back" to 1,
|
|
161
|
+
"wav_name" to "microphone",
|
|
162
|
+
"wav_format" to "pcm",
|
|
163
|
+
"audio_fs" to 16000,
|
|
164
|
+
"is_speaking" to true
|
|
165
|
+
)
|
|
166
|
+
val handshakeMsg = asrSession.textMessage(objectMapper.writeValueAsString(handshake))
|
|
167
|
+
|
|
168
|
+
// 2. 转发音频流 (Frontend -> FunASR)
|
|
169
|
+
val upstream = session.receive()
|
|
170
|
+
.filter { it.type == WebSocketMessage.Type.BINARY }
|
|
171
|
+
.map { msg ->
|
|
172
|
+
// 提取二进制数据并转发
|
|
173
|
+
val bytes = ByteArray(msg.payload.readableByteCount())
|
|
174
|
+
msg.payload.read(bytes)
|
|
175
|
+
asrSession.binaryMessage { it.wrap(bytes) }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 3. 接收结果并转换 (FunASR -> Frontend)
|
|
179
|
+
val downstream = asrSession.receive()
|
|
180
|
+
.map { it.payloadAsText }
|
|
181
|
+
.mapNotNull { json ->
|
|
182
|
+
// 解析 FunASR 响应并转换为组件所需格式
|
|
183
|
+
val node = objectMapper.readTree(json)
|
|
184
|
+
val text = node.path("text").asText()
|
|
185
|
+
val mode = node.path("mode").asText()
|
|
186
|
+
if (!text.isNullOrBlank()) {
|
|
187
|
+
val response = mapOf(
|
|
188
|
+
"text" to text,
|
|
189
|
+
"isFinal" to (mode == "2pass-offline")
|
|
190
|
+
)
|
|
191
|
+
session.textMessage(objectMapper.writeValueAsString(response))
|
|
192
|
+
} else null
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 合并流:发送握手 + 双向转发
|
|
196
|
+
// 关键修复:在前端断开或流结束时,发送 {"is_speaking": false} 以触发 2pass-offline 最终结果
|
|
197
|
+
val endSignal = Mono.defer {
|
|
198
|
+
Mono.just(asrSession.textMessage("{\"is_speaking\":false}"))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
asrSession.send(upstream.startWith(handshakeMsg).concatWith(endSignal))
|
|
202
|
+
.then(session.send(downstream))
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
119
208
|
## 主题定制
|
|
120
209
|
|
|
121
210
|
组件使用 CSS 变量进行样式定义,你可以通过覆盖这些变量来定制主题:
|
package/dist/chat-input.es.js
CHANGED
|
@@ -946,9 +946,9 @@ const sg = { class: "chat-modal-container" }, yg = {
|
|
|
946
946
|
}, JB = ["title"], cB = ["onClick"], FB = { class: "text-mode-container" }, MB = {
|
|
947
947
|
key: 0,
|
|
948
948
|
class: "tool-btn attachment-btn"
|
|
949
|
-
}, UB = ["accept", "disabled"], tB = { class: "textarea-container
|
|
949
|
+
}, UB = ["accept", "disabled"], tB = { class: "textarea-container" }, sB = {
|
|
950
950
|
key: 0,
|
|
951
|
-
class: "
|
|
951
|
+
class: "proxy-layer"
|
|
952
952
|
}, yB = ["placeholder", "disabled", "onKeydown"], iB = ["disabled"], OB = ["disabled"], rB = { class: "voice-mode-container" }, jB = { class: "voice-block-wrapper" }, KB = 150, SB = /* @__PURE__ */ d({
|
|
953
953
|
__name: "ChatInput",
|
|
954
954
|
props: {
|
|
@@ -1285,7 +1285,7 @@ const sg = { class: "chat-modal-container" }, yg = {
|
|
|
1285
1285
|
], 6)
|
|
1286
1286
|
], 6));
|
|
1287
1287
|
}
|
|
1288
|
-
}), GB = /* @__PURE__ */ EA(SB, [["__scopeId", "data-v-
|
|
1288
|
+
}), GB = /* @__PURE__ */ EA(SB, [["__scopeId", "data-v-c4107bcb"]]), xB = {
|
|
1289
1289
|
install(f) {
|
|
1290
1290
|
f.component("ChatInput", GB), f.component("VoiceRecorder", aA);
|
|
1291
1291
|
}
|