@5minds/node-red-dashboard-2-processcube-chat 0.1.1-add-functionality-5f8f20-mde6na5h
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 +1 -0
- package/nodes/ui-deepchat.html +212 -0
- package/nodes/ui-deepchat.js +25 -0
- package/package.json +47 -0
- package/resources/ui-deepchat.umd.js +273 -0
- package/resources/ui-deepchat.umd.js.map +1 -0
- package/ui/components/UIDeepChat.vue +302 -0
- package/ui/index.js +2 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="deep-chat-container">
|
|
3
|
+
<deep-chat
|
|
4
|
+
:style="deepChatStyle"
|
|
5
|
+
:textInput="textInputConfig"
|
|
6
|
+
:introMessage="introMessageConfig"
|
|
7
|
+
:connect="connectConfig"
|
|
8
|
+
:speechToText="props.speechToText"
|
|
9
|
+
:camera="props.camera"
|
|
10
|
+
:mixedFiles="props.attachments"
|
|
11
|
+
:avatars="props.avatars"
|
|
12
|
+
:names="props.names"
|
|
13
|
+
:timestamps="props.timestamps"
|
|
14
|
+
:stream="props.stream"
|
|
15
|
+
></deep-chat>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script>
|
|
20
|
+
import 'deep-chat';
|
|
21
|
+
|
|
22
|
+
export default {
|
|
23
|
+
name: 'UIDeepChat',
|
|
24
|
+
props: ['id', 'props', 'state'],
|
|
25
|
+
inject: ['$socket'],
|
|
26
|
+
data() {
|
|
27
|
+
return {
|
|
28
|
+
conversation: [],
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
computed: {
|
|
33
|
+
deepChatStyle() {
|
|
34
|
+
return {
|
|
35
|
+
width: '100%',
|
|
36
|
+
maxWidth: '600px',
|
|
37
|
+
height: '80vh',
|
|
38
|
+
borderRadius: '8px',
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
textInputConfig() {
|
|
43
|
+
return {
|
|
44
|
+
placeholder: {
|
|
45
|
+
text: this.props.placeholder || 'Type a message...',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
introMessageConfig() {
|
|
51
|
+
return {
|
|
52
|
+
text: this.props.introMessage || 'Hello! How can I help you today?',
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
connectConfig() {
|
|
57
|
+
return {
|
|
58
|
+
handler: this.handleConnection,
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
beforeUnmount() {
|
|
63
|
+
this.$socket.off('msg-input:' + this.id);
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
methods: {
|
|
67
|
+
async handleConnection(body, signals) {
|
|
68
|
+
try {
|
|
69
|
+
// Extract messages and files from FormData
|
|
70
|
+
let newMessages = [];
|
|
71
|
+
let files = [];
|
|
72
|
+
|
|
73
|
+
if (body instanceof FormData) {
|
|
74
|
+
for (let [key, value] of body.entries()) {
|
|
75
|
+
if (key.startsWith('message')) {
|
|
76
|
+
try {
|
|
77
|
+
const messageContent = JSON.parse(value);
|
|
78
|
+
newMessages.push(messageContent);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
console.error('Error parsing message:', e);
|
|
81
|
+
}
|
|
82
|
+
} else if (key === 'files') {
|
|
83
|
+
files.push(value);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Process files if present
|
|
88
|
+
if (files.length > 0 && newMessages.length > 0) {
|
|
89
|
+
const lastMessage = newMessages[newMessages.length - 1];
|
|
90
|
+
lastMessage.files = await this.processFiles(files);
|
|
91
|
+
}
|
|
92
|
+
} else if (body.messages) {
|
|
93
|
+
newMessages = body.messages;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Add to conversation history
|
|
97
|
+
this.conversation.push(...newMessages);
|
|
98
|
+
|
|
99
|
+
// Send to Node-RED
|
|
100
|
+
const payload = this.formatForChatGPT(this.conversation);
|
|
101
|
+
this.sendToNodeRED(payload, signals);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('Error in handleConnection:', error);
|
|
104
|
+
this.sendErrorResponse(signals, 'Sorry, there was an error processing your message.');
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
async processFiles(files) {
|
|
109
|
+
try {
|
|
110
|
+
return await Promise.all(
|
|
111
|
+
files.map(async (file) => {
|
|
112
|
+
if (!(file instanceof File)) return file;
|
|
113
|
+
|
|
114
|
+
if (file.type.startsWith('image/')) {
|
|
115
|
+
return await this.processImageFile(file);
|
|
116
|
+
} else if (file.type.startsWith('audio/')) {
|
|
117
|
+
return await this.processAudioFile(file);
|
|
118
|
+
} else {
|
|
119
|
+
// Handle other file types (PDFs, documents, etc.)
|
|
120
|
+
return await this.processDocumentFile(file);
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error('Error processing files:', error);
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
processImageFile(file) {
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
const reader = new FileReader();
|
|
133
|
+
reader.onload = (e) =>
|
|
134
|
+
resolve({
|
|
135
|
+
name: file.name,
|
|
136
|
+
type: file.type,
|
|
137
|
+
size: file.size,
|
|
138
|
+
src: e.target.result,
|
|
139
|
+
});
|
|
140
|
+
reader.onerror = () => reject(new Error('Failed to read image'));
|
|
141
|
+
reader.readAsDataURL(file);
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
processAudioFile(file) {
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
const reader = new FileReader();
|
|
148
|
+
reader.onload = (e) => {
|
|
149
|
+
try {
|
|
150
|
+
const base64Data = e.target.result.split(',')[1]; // Remove data URL prefix
|
|
151
|
+
|
|
152
|
+
resolve({
|
|
153
|
+
name: file.name,
|
|
154
|
+
type: file.type,
|
|
155
|
+
size: file.size,
|
|
156
|
+
base64Data: base64Data,
|
|
157
|
+
});
|
|
158
|
+
} catch (error) {
|
|
159
|
+
reject(error);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
reader.onerror = () => reject(new Error('Failed to read audio'));
|
|
163
|
+
reader.readAsDataURL(file);
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
processDocumentFile(file) {
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
const reader = new FileReader();
|
|
170
|
+
reader.onload = (e) => {
|
|
171
|
+
try {
|
|
172
|
+
const base64Data = e.target.result.split(',')[1]; // Remove data URL prefix
|
|
173
|
+
|
|
174
|
+
resolve({
|
|
175
|
+
name: file.name,
|
|
176
|
+
type: file.type,
|
|
177
|
+
size: file.size,
|
|
178
|
+
fileData: base64Data, // Use fileData for documents
|
|
179
|
+
});
|
|
180
|
+
} catch (error) {
|
|
181
|
+
reject(error);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
reader.onerror = () => reject(new Error('Failed to read document'));
|
|
185
|
+
reader.readAsDataURL(file);
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
formatForChatGPT(conversation, textOnly = false) {
|
|
190
|
+
return {
|
|
191
|
+
messages: conversation.map((msg) => {
|
|
192
|
+
if (msg.text && (!msg.files || msg.files.length === 0)) {
|
|
193
|
+
return {
|
|
194
|
+
role: msg.role === 'ai' ? 'assistant' : 'user',
|
|
195
|
+
content: msg.text,
|
|
196
|
+
};
|
|
197
|
+
} else if (msg.files && msg.files.length > 0) {
|
|
198
|
+
const content = [{ type: 'text', text: msg.text || '' }];
|
|
199
|
+
|
|
200
|
+
msg.files.forEach((file) => {
|
|
201
|
+
if (file.type && file.type.startsWith('image/') && file.src) {
|
|
202
|
+
content.push({
|
|
203
|
+
type: 'image_url',
|
|
204
|
+
image_url: { url: file.src },
|
|
205
|
+
});
|
|
206
|
+
} else if (!textOnly && file.type && file.type.startsWith('audio/') && file.base64Data) {
|
|
207
|
+
const format = this.getAudioFormat(file.type);
|
|
208
|
+
content.push({
|
|
209
|
+
type: 'input_audio',
|
|
210
|
+
input_audio: {
|
|
211
|
+
data: file.base64Data,
|
|
212
|
+
format: format,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
} else if (!textOnly && file.fileData) {
|
|
216
|
+
content.push({
|
|
217
|
+
type: 'file',
|
|
218
|
+
file: {
|
|
219
|
+
filename: file.name,
|
|
220
|
+
file_data: file.fileData,
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
role: msg.role === 'ai' ? 'assistant' : 'user',
|
|
228
|
+
content: content,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
role: msg.role === 'ai' ? 'assistant' : 'user',
|
|
234
|
+
content: msg.text || '',
|
|
235
|
+
};
|
|
236
|
+
}),
|
|
237
|
+
model: this.props.model,
|
|
238
|
+
};
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
getAudioFormat(mimeType) {
|
|
242
|
+
if (mimeType.includes('mp3')) return 'mp3';
|
|
243
|
+
if (mimeType.includes('wav')) return 'wav';
|
|
244
|
+
if (mimeType.includes('webm')) return 'webm';
|
|
245
|
+
if (mimeType.includes('m4a')) return 'm4a';
|
|
246
|
+
return 'wav';
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
sendToNodeRED(payload, signals, fallbackMessage = null) {
|
|
250
|
+
this.$socket.emit('widget-action', this.id, { payload });
|
|
251
|
+
|
|
252
|
+
const responseTimeout = setTimeout(() => {
|
|
253
|
+
console.warn('No response from Node-RED after 30 seconds');
|
|
254
|
+
this.sendErrorResponse(signals, fallbackMessage || 'No response from server. Please try again.');
|
|
255
|
+
}, 30000);
|
|
256
|
+
|
|
257
|
+
this.$socket.once('msg-input:' + this.id, (msg) => {
|
|
258
|
+
clearTimeout(responseTimeout);
|
|
259
|
+
this.handleNodeREDResponse(msg, signals);
|
|
260
|
+
});
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
handleNodeREDResponse(msg, signals) {
|
|
264
|
+
try {
|
|
265
|
+
const aiMessage = {
|
|
266
|
+
text: msg.payload.text || msg.payload.content,
|
|
267
|
+
role: 'ai',
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
this.conversation.push(aiMessage);
|
|
271
|
+
|
|
272
|
+
if (msg.payload) {
|
|
273
|
+
signals.onResponse({
|
|
274
|
+
text: msg.payload.text || msg.payload.content,
|
|
275
|
+
role: 'ai',
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
} catch (error) {
|
|
279
|
+
console.error('Error handling response:', error);
|
|
280
|
+
this.sendErrorResponse(signals, 'Error processing response');
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
sendErrorResponse(signals, message) {
|
|
285
|
+
if (signals && signals.onResponse) {
|
|
286
|
+
signals.onResponse({
|
|
287
|
+
text: message,
|
|
288
|
+
role: 'ai',
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
</script>
|
|
295
|
+
|
|
296
|
+
<style scoped>
|
|
297
|
+
.deep-chat-container {
|
|
298
|
+
display: flex;
|
|
299
|
+
justify-content: center;
|
|
300
|
+
width: 100%;
|
|
301
|
+
}
|
|
302
|
+
</style>
|
package/ui/index.js
ADDED