3
3
#include < QFileDialog>
4
4
#include < QHBoxLayout>
5
5
#include < QMimeData>
6
+ #include < QTabBar>
7
+ #include < QTabWidget>
6
8
#include < QTextEdit>
7
9
#include < QToolButton>
8
10
11
+ #include < utils/filepath.h>
12
+ #include < utils/fsengine/fileiconprovider.h>
13
+
9
14
#include " llamachatinput.h"
10
15
#include " llamatheme.h"
11
16
#include " llamatr.h"
12
17
18
+ using namespace Utils ;
19
+
13
20
namespace LlamaCpp {
14
21
22
+ static bool isImageFile (const QString &fileName)
23
+ {
24
+ QString ext = QFileInfo (fileName).suffix ().toLower ();
25
+ return ext == " png" || ext == " jpg" || ext == " jpeg" || ext == " webp" || ext == " svg" ;
26
+ }
27
+
28
+ static bool isAudioFile (const QString &fileName)
29
+ {
30
+ QString ext = QFileInfo (fileName).suffix ().toLower ();
31
+ return ext == " wav" || ext == " mp3" ;
32
+ }
33
+
34
+ static QString mimeNameFromExtension (const QString &ext)
35
+ {
36
+ if (ext == " png" )
37
+ return " image/png" ;
38
+ if (ext == " jpg" || ext == " jpeg" )
39
+ return " image/jpeg" ;
40
+ if (ext == " webp" )
41
+ return " image/webp" ;
42
+ if (ext == " svg" )
43
+ return " image/svg+xml" ;
44
+ if (ext == " wav" )
45
+ return " audio/wav" ;
46
+ if (ext == " mp3" )
47
+ return " audio/mpeg" ;
48
+ return " application/octet-stream" ;
49
+ }
50
+
15
51
ChatInput::ChatInput (QWidget *parent)
16
52
: QWidget(parent)
17
53
{
@@ -23,12 +59,16 @@ ChatInput::ChatInput(QWidget *parent)
23
59
24
60
void ChatInput::buildUI ()
25
61
{
26
- auto main = new QHBoxLayout (this );
27
- main->setContentsMargins (10 , 10 , 10 , 10 );
62
+ auto main = new QVBoxLayout (this );
63
+ main->setContentsMargins (0 , 0 , 0 , 0 );
64
+
65
+ auto textAndButtonLayout = new QHBoxLayout;
66
+ textAndButtonLayout->setContentsMargins (10 , 10 , 10 , 10 );
28
67
29
68
m_txt = new QTextEdit (this );
30
69
m_txt->setPlaceholderText (Tr::tr (" Type a message (Shift+Enter for new line)" ));
31
70
m_txt->setAcceptRichText (false );
71
+ m_txt->setAcceptDrops (false );
32
72
33
73
m_txt->installEventFilter (this );
34
74
installEventFilter (this );
@@ -40,9 +80,9 @@ void ChatInput::buildUI()
40
80
m_attachButton->setIcon (QIcon::fromTheme (" mail-attachment" ));
41
81
m_attachButton->setToolTip (Tr::tr (" Attach file" ));
42
82
connect (m_attachButton, &QToolButton::clicked, [this ]() {
43
- QStringList files = QFileDialog::getOpenFileNames (this );
83
+ const QStringList files = QFileDialog::getOpenFileNames (this );
44
84
if (!files.isEmpty ())
45
- emit fileDropped (files);
85
+ addFilesFromLocalPaths (files);
46
86
});
47
87
48
88
m_sendStopButton = new QToolButton (this );
@@ -56,8 +96,35 @@ void ChatInput::buildUI()
56
96
btnLayout->addWidget (m_attachButton);
57
97
btnLayout->addWidget (m_sendStopButton);
58
98
59
- main->addWidget (m_txt);
60
- main->addLayout (btnLayout);
99
+ textAndButtonLayout->addWidget (m_txt);
100
+ textAndButtonLayout->addLayout (btnLayout);
101
+
102
+ m_attachedFilesBar = new QTabBar (this );
103
+ m_attachedFilesBar->setAcceptDrops (false );
104
+ m_attachedFilesBar->setVisible (false );
105
+ m_attachedFilesBar->setTabsClosable (true );
106
+ m_attachedFilesBar->setDocumentMode (true );
107
+ m_attachedFilesBar->setExpanding (false );
108
+ m_attachedFilesBar->setDrawBase (false );
109
+ m_attachedFilesBar->setElideMode (Qt::ElideMiddle);
110
+
111
+ QSizePolicy sp (QSizePolicy::Preferred, QSizePolicy::Fixed);
112
+ sp.setHorizontalStretch (1 );
113
+ sp.setVerticalStretch (0 );
114
+ sp.setHeightForWidth (sizePolicy ().hasHeightForWidth ());
115
+ m_attachedFilesBar->setSizePolicy (sp);
116
+
117
+ connect (m_attachedFilesBar, &QTabBar::tabCloseRequested, this , [this ](int index) {
118
+ m_attachedFilesBar->removeTab (index);
119
+ m_attachedFiles.removeAt (index);
120
+ if (m_attachedFilesBar->count () == 0 ) {
121
+ m_attachedFilesBar->setVisible (false );
122
+ updateMaximumHeight ();
123
+ }
124
+ });
125
+
126
+ main->addLayout (textAndButtonLayout);
127
+ main->addWidget (m_attachedFilesBar);
61
128
62
129
updateUI ();
63
130
}
@@ -76,7 +143,6 @@ void ChatInput::updateUI()
76
143
void ChatInput::applyStyleSheet ()
77
144
{
78
145
setAttribute (Qt::WA_StyledBackground, true );
79
-
80
146
setStyleSheet (replaceThemeColorNamesWithRGBNames (R"(
81
147
QWidget#ChatInput {
82
148
background-color: Token_Background_Muted;
@@ -96,25 +162,107 @@ void ChatInput::applyStyleSheet()
96
162
border-radius: 6px;
97
163
padding: 4px 4px;
98
164
}
165
+
99
166
QToolButton:hover {
100
167
background-color: Token_Foreground_Muted;
101
168
}
169
+
170
+ QTabBar {
171
+ background-color: Token_Background_Muted;
172
+ height: 22px;
173
+ margin-left: 10px;
174
+ margin-right: 10px;
175
+ margin-bottom: 1px;
176
+ }
177
+
178
+ QTabBar::tear {
179
+ width: 0px;
180
+ }
181
+
182
+ QTabBar::close-button {
183
+ subcontrol-position: right;
184
+ }
185
+
186
+ QTabBar::tab {
187
+ background-color: Token_Background_Muted;
188
+ color: Token_Text_Default;
189
+
190
+ border: 1px solid Token_Foreground_Muted;
191
+ border-radius: 6px;
192
+ height: 20px;
193
+
194
+ min-width: 5ex;
195
+
196
+ margin-top: 4px;
197
+ margin-bottom: 4px;
198
+ margin-left: 4px;
199
+ }
200
+
201
+ QTabBar::tab:hover {
202
+ background-color: Token_Foreground_Muted;
203
+ }
204
+
102
205
)" ));
103
206
}
104
207
208
+ void ChatInput::cleanUp ()
209
+ {
210
+ m_txt->clear ();
211
+
212
+ while (m_attachedFilesBar->count () > 0 ) {
213
+ m_attachedFilesBar->removeTab (0 );
214
+ }
215
+ m_attachedFilesBar->setVisible (false );
216
+
217
+ m_attachedFiles.clear ();
218
+
219
+ updateMaximumHeight ();
220
+ }
221
+
105
222
void ChatInput::onSendClicked ()
106
223
{
107
224
QString message = m_txt->toPlainText ().trimmed ();
108
225
if (!message.isEmpty ())
109
- emit sendRequested (message);
110
- m_txt-> clear ();
226
+ emit sendRequested (message, getExtraFromAttachedFiles () );
227
+ cleanUp ();
111
228
}
112
229
113
230
void ChatInput::onStopClicked ()
114
231
{
115
232
emit stopRequested ();
116
233
}
117
234
235
+ void ChatInput::addFilesFromLocalPaths (const QStringList &filePaths)
236
+ {
237
+ for (const QString &path : filePaths) {
238
+ FilePath localFile = FilePath::fromString (path);
239
+ if (!localFile.isLocal ())
240
+ continue ; // skip non‑local files
241
+
242
+ const QString fileName = localFile.fileName ();
243
+
244
+ // If a file with the same name already exists, just replace its
245
+ // contents – we don't want duplicate tabs.
246
+ auto existing = std::ranges::find_if (m_attachedFiles, [fileName](const auto &pair) {
247
+ return pair.first == fileName;
248
+ });
249
+ if (existing != m_attachedFiles.end ()) {
250
+ existing->second = localFile.fileContents ().value_or (" " );
251
+ continue ;
252
+ }
253
+
254
+ const QIcon icon = FileIconProvider::icon (localFile);
255
+ m_attachedFilesBar->addTab (icon, localFile.fileName ());
256
+
257
+ m_attachedFiles.append ({localFile.fileName (), localFile.fileContents ().value_or (" " )});
258
+ }
259
+
260
+ if (!filePaths.isEmpty ()) {
261
+ m_attachedFilesBar->setVisible (true );
262
+ updateMaximumHeight ();
263
+ }
264
+ }
265
+
118
266
void ChatInput::dragEnterEvent (QDragEnterEvent *e)
119
267
{
120
268
if (e->mimeData ()->hasUrls ())
@@ -123,10 +271,16 @@ void ChatInput::dragEnterEvent(QDragEnterEvent *e)
123
271
124
272
void ChatInput::dropEvent (QDropEvent *e)
125
273
{
126
- QStringList fileList;
127
- for (const QUrl &url : e->mimeData ()->urls ())
128
- fileList << url.toLocalFile ();
129
- emit fileDropped (fileList);
274
+ QStringList localPaths;
275
+ for (const QUrl &url : e->mimeData ()->urls ()) {
276
+ FilePath fp = FilePath::fromUrl (url);
277
+ if (fp.isLocal ())
278
+ localPaths.append (fp.toFSPathString ());
279
+ }
280
+
281
+ addFilesFromLocalPaths (localPaths);
282
+
283
+ e->acceptProposedAction ();
130
284
}
131
285
132
286
bool ChatInput::eventFilter (QObject *obj, QEvent *event)
@@ -147,6 +301,7 @@ bool ChatInput::eventFilter(QObject *obj, QEvent *event)
147
301
return true ;
148
302
}
149
303
}
304
+
150
305
if (obj == this && event->type () == QEvent::FocusIn) {
151
306
m_txt->setFocus ();
152
307
return true ;
@@ -161,10 +316,76 @@ void ChatInput::setIsGenerating(bool newIsGenerating)
161
316
updateUI ();
162
317
}
163
318
164
- void ChatInput::setEditingText (const QString &editingText)
319
+ void ChatInput::setEditingText (const QString &editingText, const QList<QVariantMap> &extra )
165
320
{
321
+ cleanUp ();
322
+
323
+ for (const QVariantMap &e : extra) {
324
+ if (e.value (" type" ).toString () == " textFile" ) {
325
+ const QString fileName = e.value (" name" ).toString ();
326
+ m_attachedFiles.append ({fileName, e.value (" content" ).toByteArray ()});
327
+
328
+ const QIcon icon = FileIconProvider::icon (FilePath::fromString (fileName));
329
+ m_attachedFilesBar->addTab (icon, fileName);
330
+ } else if (e.value (" type" ).toString () == " imageFile" ) {
331
+ const QString fileName = e.value (" name" ).toString ();
332
+ m_attachedFiles.append ({fileName, e.value (" content" ).toByteArray ()});
333
+
334
+ const QIcon icon = FileIconProvider::icon (FilePath::fromString (fileName));
335
+ m_attachedFilesBar->addTab (icon, fileName);
336
+ } else if (e.value (" type" ).toString () == " audioFile" ) {
337
+ const QString fileName = e.value (" name" ).toString ();
338
+ m_attachedFiles.append ({fileName, e.value (" content" ).toByteArray ()});
339
+
340
+ const QIcon icon = FileIconProvider::icon (FilePath::fromString (fileName));
341
+ m_attachedFilesBar->addTab (icon, fileName);
342
+ }
343
+ }
344
+
345
+ m_attachedFilesBar->setVisible (m_attachedFilesBar->count () > 0 );
346
+ updateMaximumHeight ();
347
+
166
348
m_txt->setText (editingText);
167
349
m_txt->setFocus ();
168
350
}
169
351
352
+ void ChatInput::updateMaximumHeight ()
353
+ {
354
+ int maximumHeight = 80 ;
355
+ if (m_attachedFilesBar->isVisible ())
356
+ maximumHeight += 20 ;
357
+ setMaximumHeight (maximumHeight);
358
+ }
359
+
360
+ QList<QVariantMap> ChatInput::getExtraFromAttachedFiles ()
361
+ {
362
+ QList<QVariantMap> extraList;
363
+ for (const auto &fileAndContent : std::as_const (m_attachedFiles)) {
364
+ QVariantMap extra;
365
+ const QString fileName = fileAndContent.first ;
366
+ const QByteArray content = fileAndContent.second ;
367
+ QString ext = QFileInfo (fileName).suffix ().toLower ();
368
+
369
+ if (isImageFile (fileName)) {
370
+ QString base64Url = " data:" + mimeNameFromExtension (ext) + " ;base64,"
371
+ + content.toBase64 ();
372
+ extra[" type" ] = " imageFile" ;
373
+ extra[" name" ] = fileName;
374
+ extra[" base64Url" ] = base64Url;
375
+ } else if (isAudioFile (fileName)) {
376
+ QString base64Url = " data:" + mimeNameFromExtension (ext) + " ;base64,"
377
+ + content.toBase64 ();
378
+ extra[" type" ] = " audioFile" ;
379
+ extra[" name" ] = fileName;
380
+ extra[" base64Url" ] = base64Url;
381
+ } else {
382
+ extra[" type" ] = " textFile" ;
383
+ extra[" name" ] = fileName;
384
+ extra[" content" ] = content;
385
+ }
386
+ extraList << extra;
387
+ }
388
+ return extraList;
389
+ }
390
+
170
391
} // namespace LlamaCpp
0 commit comments