Skip to content

Commit bf9cd4c

Browse files
committed
Add support for drag n drop and pasting images
1 parent b5b0b79 commit bf9cd4c

3 files changed

Lines changed: 145 additions & 3 deletions

File tree

llms/ui/ChatPrompt.mjs

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,18 @@ export default {
7777
v-model="messageText"
7878
@keydown.enter.exact.prevent="sendMessage"
7979
@keydown.enter.shift.exact="addNewLine"
80-
placeholder="Type your message... (Enter to send, Shift+Enter for new line)"
80+
@paste="onPaste"
81+
@dragover="onDragOver"
82+
@dragleave="onDragLeave"
83+
@drop="onDrop"
84+
placeholder="Type your message... (Enter to send, Shift+Enter for new line, drag & drop or paste files)"
8185
rows="3"
82-
class="block w-full rounded-md border border-gray-300 px-3 py-2 pr-12 text-sm placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
86+
:class="[
87+
'block w-full rounded-md border px-3 py-2 pr-12 text-sm placeholder-gray-500 focus:outline-none focus:ring-1',
88+
isDragging
89+
? 'border-blue-500 bg-blue-50 ring-1 ring-blue-500'
90+
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
91+
]"
8392
:disabled="isGenerating || !model"
8493
></textarea>
8594
<button title="Send (Enter)" type="button"
@@ -169,6 +178,93 @@ export default {
169178
attachedFiles.value.splice(i, 1)
170179
}
171180

181+
// Helper function to add files and set default message
182+
const addFilesAndSetMessage = (files) => {
183+
if (files.length === 0) return
184+
185+
attachedFiles.value.push(...files)
186+
187+
// Set default message text if empty
188+
if (!messageText.value.trim()) {
189+
if (hasImage()) {
190+
messageText.value = getTextContent(config.defaults.image)
191+
} else if (hasAudio()) {
192+
messageText.value = getTextContent(config.defaults.audio)
193+
} else {
194+
messageText.value = getTextContent(config.defaults.file)
195+
}
196+
}
197+
}
198+
199+
// Handle paste events for clipboard images, audio, and files
200+
const onPaste = async (e) => {
201+
// Use the paste event's clipboardData directly (works best for paste events)
202+
const items = e.clipboardData?.items
203+
if (!items) return
204+
205+
const files = []
206+
207+
// Check all clipboard items
208+
for (let i = 0; i < items.length; i++) {
209+
const item = items[i]
210+
211+
// Handle files (images, audio, etc.)
212+
if (item.kind === 'file') {
213+
const file = item.getAsFile()
214+
if (file) {
215+
// Generate a better filename based on type
216+
let filename = file.name
217+
if (!filename || filename === 'image.png' || filename === 'blob') {
218+
const ext = file.type.split('/')[1] || 'png'
219+
const timestamp = new Date().getTime()
220+
if (file.type.startsWith('image/')) {
221+
filename = `pasted-image-${timestamp}.${ext}`
222+
} else if (file.type.startsWith('audio/')) {
223+
filename = `pasted-audio-${timestamp}.${ext}`
224+
} else {
225+
filename = `pasted-file-${timestamp}.${ext}`
226+
}
227+
// Create a new File object with the better name
228+
files.push(new File([file], filename, { type: file.type }))
229+
} else {
230+
files.push(file)
231+
}
232+
}
233+
}
234+
}
235+
236+
if (files.length > 0) {
237+
e.preventDefault()
238+
addFilesAndSetMessage(files)
239+
}
240+
}
241+
242+
// Handle drag and drop events
243+
const isDragging = ref(false)
244+
245+
const onDragOver = (e) => {
246+
e.preventDefault()
247+
e.stopPropagation()
248+
isDragging.value = true
249+
}
250+
251+
const onDragLeave = (e) => {
252+
e.preventDefault()
253+
e.stopPropagation()
254+
isDragging.value = false
255+
}
256+
257+
const onDrop = (e) => {
258+
e.preventDefault()
259+
e.stopPropagation()
260+
isDragging.value = false
261+
262+
const files = Array.from(e.dataTransfer?.files || [])
263+
if (files.length > 0) {
264+
addFilesAndSetMessage(files)
265+
}
266+
}
267+
172268
function createChatRequest() {
173269
if (hasImage()) {
174270
return deepClone(config.defaults.image)
@@ -433,8 +529,13 @@ export default {
433529
messageText,
434530
fileInput,
435531
showSettings,
532+
isDragging,
436533
triggerFilePicker,
437534
onFilesSelected,
535+
onPaste,
536+
onDragOver,
537+
onDragLeave,
538+
onDrop,
438539
removeAttachment,
439540
sendMessage,
440541
addNewLine,

llms/ui/ModelSelector.mjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import ProviderStatus from "./ProviderStatus.mjs"
22
import ProviderIcon from "./ProviderIcon.mjs"
3-
import { useFormatters } from "@servicestack/vue"
43

54
export default {
65
components: {

llms/ui/app.css

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,9 +393,15 @@
393393
max-width: 96rem;
394394
}
395395
}
396+
.-m-2 {
397+
margin: calc(var(--spacing) * -2);
398+
}
396399
.-m-2\.5 {
397400
margin: calc(var(--spacing) * -2.5);
398401
}
402+
.-mx-1 {
403+
margin-inline: calc(var(--spacing) * -1);
404+
}
399405
.-mx-1\.5 {
400406
margin-inline: calc(var(--spacing) * -1.5);
401407
}
@@ -408,6 +414,9 @@
408414
.mx-auto {
409415
margin-inline: auto;
410416
}
417+
.-my-1 {
418+
margin-block: calc(var(--spacing) * -1);
419+
}
411420
.-my-1\.5 {
412421
margin-block: calc(var(--spacing) * -1.5);
413422
}
@@ -483,6 +492,9 @@
483492
.-ml-px {
484493
margin-left: -1px;
485494
}
495+
.ml-0 {
496+
margin-left: calc(var(--spacing) * 0);
497+
}
486498
.ml-0\.5 {
487499
margin-left: calc(var(--spacing) * 0.5);
488500
}
@@ -1073,6 +1085,9 @@
10731085
.border-yellow-400 {
10741086
border-color: var(--color-yellow-400);
10751087
}
1088+
.bg-black {
1089+
background-color: var(--color-black);
1090+
}
10761091
.bg-black\/40 {
10771092
background-color: color-mix(in srgb, #000 40%, transparent);
10781093
@supports (color: color-mix(in lab, red, red)) {
@@ -1113,6 +1128,9 @@
11131128
.bg-gray-400 {
11141129
background-color: var(--color-gray-400);
11151130
}
1131+
.bg-gray-500 {
1132+
background-color: var(--color-gray-500);
1133+
}
11161134
.bg-gray-500\/75 {
11171135
background-color: color-mix(in srgb, oklch(55.1% 0.027 264.364) 75%, transparent);
11181136
@supports (color: color-mix(in lab, red, red)) {
@@ -1127,6 +1145,9 @@
11271145
.bg-gray-700 {
11281146
background-color: var(--color-gray-700);
11291147
}
1148+
.bg-gray-900 {
1149+
background-color: var(--color-gray-900);
1150+
}
11301151
.bg-gray-900\/80 {
11311152
background-color: color-mix(in srgb, oklch(21% 0.034 264.665) 80%, transparent);
11321153
@supports (color: color-mix(in lab, red, red)) {
@@ -1174,6 +1195,9 @@
11741195
.bg-sky-600 {
11751196
background-color: var(--color-sky-600);
11761197
}
1198+
.bg-slate-400 {
1199+
background-color: var(--color-slate-400);
1200+
}
11771201
.bg-slate-400\/10 {
11781202
background-color: color-mix(in srgb, oklch(70.4% 0.04 256.788) 10%, transparent);
11791203
@supports (color: color-mix(in lab, red, red)) {
@@ -1236,6 +1260,9 @@
12361260
.px-6 {
12371261
padding-inline: calc(var(--spacing) * 6);
12381262
}
1263+
.py-0 {
1264+
padding-block: calc(var(--spacing) * 0);
1265+
}
12391266
.py-0\.5 {
12401267
padding-block: calc(var(--spacing) * 0.5);
12411268
}
@@ -1266,6 +1293,9 @@
12661293
.py-12 {
12671294
padding-block: calc(var(--spacing) * 12);
12681295
}
1296+
.pt-0 {
1297+
padding-top: calc(var(--spacing) * 0);
1298+
}
12691299
.pt-0\.5 {
12701300
padding-top: calc(var(--spacing) * 0.5);
12711301
}
@@ -1589,6 +1619,9 @@
15891619
.uppercase {
15901620
text-transform: uppercase;
15911621
}
1622+
.underline {
1623+
text-decoration-line: underline;
1624+
}
15921625
.placeholder-gray-500 {
15931626
&::placeholder {
15941627
color: var(--color-gray-500);
@@ -1650,6 +1683,9 @@
16501683
--tw-inset-ring-shadow: inset 0 0 0 1px var(--tw-inset-ring-color, currentcolor);
16511684
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
16521685
}
1686+
.ring-black {
1687+
--tw-ring-color: var(--color-black);
1688+
}
16531689
.ring-black\/5 {
16541690
--tw-ring-color: color-mix(in srgb, #000 5%, transparent);
16551691
@supports (color: color-mix(in lab, red, red)) {
@@ -1658,9 +1694,15 @@
16581694
}
16591695
}
16601696
}
1697+
.ring-blue-500 {
1698+
--tw-ring-color: var(--color-blue-500);
1699+
}
16611700
.ring-indigo-500 {
16621701
--tw-ring-color: var(--color-indigo-500);
16631702
}
1703+
.inset-ring-gray-900 {
1704+
--tw-inset-ring-color: var(--color-gray-900);
1705+
}
16641706
.inset-ring-gray-900\/5 {
16651707
--tw-inset-ring-color: color-mix(in srgb, oklch(21% 0.034 264.665) 5%, transparent);
16661708
@supports (color: color-mix(in lab, red, red)) {

0 commit comments

Comments
 (0)