Skip to content

Commit 580d7ac

Browse files
committed
paste and drag&drop files/images
1 parent ed05edf commit 580d7ac

3 files changed

Lines changed: 184 additions & 1 deletion

File tree

src/MaIN.InferPage/Components/Pages/Home.razor

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,16 @@
1818

1919
<ErrorNotification @bind-ErrorMessage="_errorMessage" />
2020

21-
<div class="chat-container">
21+
<div class="chat-container" id="chat-container">
22+
@if (_isDragging)
23+
{
24+
<div class="drop-overlay">
25+
<div class="drop-overlay-content">
26+
<FluentIcon Value="@(new Icons.Regular.Size48.ArrowDownload())" Style="fill: var(--accent-base-color);" />
27+
<span>Drop files here</span>
28+
</div>
29+
</div>
30+
}
2231

2332
<FluentProgress Visible="@(_isLoading)" Style="width: 100%; margin: 0; color: var(--accent-base-color)"/>
2433

@@ -186,6 +195,7 @@
186195
@code {
187196
private bool _isLoading;
188197
private bool _isThinking;
198+
private bool _isDragging;
189199
private bool _reasoning;
190200
private bool _preserveScroll;
191201
private string _accentColor = "#00cca3";
@@ -199,6 +209,7 @@
199209
private List<MessageExt> Messages { get; set; } = new();
200210
private ElementReference? _bottomElement;
201211
private ElementReference _editorRef;
212+
private DotNetObjectReference<Home>? _dotNetRef;
202213
private List<FileInfo> _selectedFiles = new();
203214
private int _inputKey;
204215

@@ -215,6 +226,11 @@
215226
_accentColor = (theme == "dark" || theme == "system-dark") ? "#00ffcc" : "#00cca3";
216227
await JS.InvokeVoidAsync("scrollManager.attachScrollListener", "messages-container");
217228
await JS.InvokeVoidAsync("scrollManager.restoreScrollPosition", "messages-container");
229+
230+
_dotNetRef = DotNetObjectReference.Create(this);
231+
await JS.InvokeVoidAsync("editorManager.attachPasteHandler", _editorRef, _dotNetRef);
232+
await JS.InvokeVoidAsync("editorManager.attachDropZone", "chat-container", _dotNetRef);
233+
218234
StateHasChanged();
219235
}
220236
else if (_preserveScroll)
@@ -313,6 +329,35 @@
313329
_selectedFiles.Remove(file);
314330
}
315331

332+
[JSInvokable]
333+
public async Task OnFilePasted(string fileName, string extension, string base64Data)
334+
{
335+
var bytes = Convert.FromBase64String(base64Data);
336+
var ms = new MemoryStream(bytes);
337+
_selectedFiles.Add(new FileInfo
338+
{
339+
Name = fileName,
340+
Extension = extension,
341+
StreamContent = ms
342+
});
343+
_isDragging = false;
344+
await InvokeAsync(StateHasChanged);
345+
}
346+
347+
[JSInvokable]
348+
public async Task OnDragEnter()
349+
{
350+
_isDragging = true;
351+
await InvokeAsync(StateHasChanged);
352+
}
353+
354+
[JSInvokable]
355+
public async Task OnDragLeave()
356+
{
357+
_isDragging = false;
358+
await InvokeAsync(StateHasChanged);
359+
}
360+
316361
private void HandleStop()
317362
{
318363
_cancellationTokenSource?.Cancel();
@@ -514,5 +559,6 @@
514559
public void Dispose()
515560
{
516561
_cancellationTokenSource?.Dispose();
562+
_dotNetRef?.Dispose();
517563
}
518564
}

src/MaIN.InferPage/wwwroot/editor.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,106 @@ window.editorManager = {
88
clickElement: (id) => {
99
const el = document.getElementById(id);
1010
if (el) el.click();
11+
},
12+
attachPasteHandler: (element, dotNetHelper) => {
13+
// Handle paste only
14+
element.addEventListener('paste', async (e) => {
15+
let imageFile = null;
16+
17+
if (e.clipboardData?.files?.length > 0) {
18+
for (const file of e.clipboardData.files) {
19+
if (file.type.startsWith('image/')) {
20+
imageFile = file;
21+
break;
22+
}
23+
}
24+
}
25+
26+
if (!imageFile && e.clipboardData?.items) {
27+
for (const item of e.clipboardData.items) {
28+
if (item.type.startsWith('image/')) {
29+
imageFile = item.getAsFile();
30+
break;
31+
}
32+
}
33+
}
34+
35+
if (!imageFile) return;
36+
37+
e.preventDefault();
38+
await editorManager._processFile(imageFile, dotNetHelper);
39+
});
40+
},
41+
attachDropZone: (containerId, dotNetHelper) => {
42+
const container = document.getElementById(containerId);
43+
if (!container) return;
44+
45+
let dragCounter = 0;
46+
47+
container.addEventListener('dragenter', async (e) => {
48+
e.preventDefault();
49+
e.stopPropagation();
50+
dragCounter++;
51+
if (dragCounter === 1) {
52+
await dotNetHelper.invokeMethodAsync('OnDragEnter');
53+
}
54+
});
55+
56+
container.addEventListener('dragleave', async (e) => {
57+
e.preventDefault();
58+
e.stopPropagation();
59+
dragCounter--;
60+
if (dragCounter === 0) {
61+
await dotNetHelper.invokeMethodAsync('OnDragLeave');
62+
}
63+
});
64+
65+
container.addEventListener('dragover', (e) => {
66+
e.preventDefault();
67+
e.stopPropagation();
68+
});
69+
70+
container.addEventListener('drop', async (e) => {
71+
e.preventDefault();
72+
e.stopPropagation();
73+
dragCounter = 0;
74+
75+
const files = e.dataTransfer?.files;
76+
if (!files || files.length === 0) {
77+
await dotNetHelper.invokeMethodAsync('OnDragLeave');
78+
return;
79+
}
80+
81+
for (const file of files) {
82+
await editorManager._processFile(file, dotNetHelper);
83+
}
84+
});
85+
},
86+
_processFile: async (file, dotNetHelper) => {
87+
try {
88+
const arrayBuffer = await file.arrayBuffer();
89+
const uint8Array = new Uint8Array(arrayBuffer);
90+
91+
// Convert to base64 - much smaller than int array
92+
let binary = '';
93+
for (let i = 0; i < uint8Array.length; i++) {
94+
binary += String.fromCharCode(uint8Array[i]);
95+
}
96+
const base64 = btoa(binary);
97+
98+
let extension = '';
99+
const lastDot = file.name.lastIndexOf('.');
100+
if (lastDot > 0) {
101+
extension = file.name.substring(lastDot);
102+
} else if (file.type) {
103+
extension = '.' + file.type.split('/')[1].replace('jpeg', 'jpg');
104+
}
105+
106+
const fileName = file.name || `file-${Date.now()}${extension}`;
107+
108+
await dotNetHelper.invokeMethodAsync('OnFilePasted', fileName, extension, base64);
109+
} catch {
110+
// Silent fail
111+
}
11112
}
12113
};

src/MaIN.InferPage/wwwroot/home.css

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,42 @@ body {
1313
flex-grow: 1;
1414
gap: 4px;
1515
padding-top: 4px;
16+
position: relative;
17+
}
18+
19+
/* Drop overlay */
20+
.drop-overlay {
21+
position: absolute;
22+
top: 0;
23+
left: 0;
24+
right: 0;
25+
bottom: 0;
26+
background: rgba(0, 204, 163, 0.15);
27+
border: 3px dashed var(--accent-base-color);
28+
border-radius: 8px;
29+
z-index: 1000;
30+
display: flex;
31+
align-items: center;
32+
justify-content: center;
33+
pointer-events: none;
34+
backdrop-filter: blur(2px);
35+
}
36+
37+
.drop-overlay-content {
38+
display: flex;
39+
flex-direction: column;
40+
align-items: center;
41+
gap: 12px;
42+
padding: 24px 48px;
43+
background: var(--neutral-layer-1);
44+
border-radius: 12px;
45+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
46+
}
47+
48+
.drop-overlay-content span {
49+
font-size: 18px;
50+
font-weight: 500;
51+
color: var(--accent-base-color);
1652
}
1753

1854
.messages-container>div {

0 commit comments

Comments
 (0)