Skip to content

Commit 23c5c3f

Browse files
committed
feat(ui): add video attachment support to the chat UI
Mirror the image/audio attachment path for video: emit video_url content parts, accept video/* in the picker, keep video files as base64, show a film icon badge, and render attached video inline with a <video> player. Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 35ff935 commit 23c5c3f

2 files changed

Lines changed: 13 additions & 4 deletions

File tree

core/http/react-ui/src/hooks/useChat.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,12 @@ export function useChat(initialModel = '') {
216216
audio_url: { url: `data:${file.type};base64,${file.base64}` },
217217
})
218218
userFiles.push({ name: file.name, type: 'audio' })
219+
} else if (file.type?.startsWith('video/')) {
220+
messageContent.push({
221+
type: 'video_url',
222+
video_url: { url: `data:${file.type};base64,${file.base64}` },
223+
})
224+
userFiles.push({ name: file.name, type: 'video' })
219225
} else {
220226
// Text/PDF files - append to content
221227
if (file.textContent) {

core/http/react-ui/src/pages/Chat.jsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ function UserMessageContent({ content, files }) {
265265
<div className="chat-message-files">
266266
{files.map((f, i) => (
267267
<span key={i} className="chat-file-inline">
268-
<i className={`fas ${f.type === 'image' ? 'fa-image' : f.type === 'audio' ? 'fa-headphones' : 'fa-file'}`} />
268+
<i className={`fas ${f.type === 'image' ? 'fa-image' : f.type === 'audio' ? 'fa-headphones' : f.type === 'video' ? 'fa-film' : 'fa-file'}`} />
269269
{f.name}
270270
</span>
271271
))}
@@ -274,6 +274,9 @@ function UserMessageContent({ content, files }) {
274274
{Array.isArray(content) && content.filter(c => c.type === 'image_url').map((img, i) => (
275275
<img key={i} src={img.image_url.url} alt="attached" className="chat-inline-image" />
276276
))}
277+
{Array.isArray(content) && content.filter(c => c.type === 'video_url').map((vid, i) => (
278+
<video key={i} src={vid.video_url.url} controls className="chat-inline-video" />
279+
))}
277280
</>
278281
)
279282
}
@@ -711,7 +714,7 @@ export default function Chat() {
711714
for (const file of e.target.files) {
712715
const base64 = await fileToBase64(file)
713716
const entry = { name: file.name, type: file.type, base64 }
714-
if (!file.type.startsWith('image/') && !file.type.startsWith('audio/')) {
717+
if (!file.type.startsWith('image/') && !file.type.startsWith('audio/') && !file.type.startsWith('video/')) {
715718
entry.textContent = await file.text().catch(() => '')
716719
}
717720
newFiles.push(entry)
@@ -1244,7 +1247,7 @@ export default function Chat() {
12441247
<div className="chat-files">
12451248
{files.map((f, i) => (
12461249
<span key={i} className="chat-file-badge">
1247-
<i className={`fas ${f.type?.startsWith('image/') ? 'fa-image' : f.type?.startsWith('audio/') ? 'fa-headphones' : 'fa-file'}`} />
1250+
<i className={`fas ${f.type?.startsWith('image/') ? 'fa-image' : f.type?.startsWith('audio/') ? 'fa-headphones' : f.type?.startsWith('video/') ? 'fa-film' : 'fa-file'}`} />
12481251
{f.name}
12491252
<button onClick={() => setFiles(prev => prev.filter((_, idx) => idx !== i))}>
12501253
<i className="fas fa-xmark" />
@@ -1343,7 +1346,7 @@ export default function Chat() {
13431346
ref={fileInputRef}
13441347
type="file"
13451348
multiple
1346-
accept="image/*,audio/*,application/pdf,.txt,.md,.csv,.json"
1349+
accept="image/*,audio/*,video/*,application/pdf,.txt,.md,.csv,.json"
13471350
style={{ display: 'none' }}
13481351
onChange={handleFileChange}
13491352
/>

0 commit comments

Comments
 (0)