Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 100 additions & 2 deletions src/components/ChattyLLM/InputArea.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="input-area">
<div class="input-area"
:class="{
draggedOver: isDraggedOver,
}"
@dragover="onDragOver"
@dragenter="onDragEnter"
@dragleave="onDragLeave"
@drop="onDrop">
<NcRichContenteditable ref="richContenteditable"
:class="{ 'input-area__thinking': loading.llmGeneration }"
class="input"
:model-value="chatContent"
:auto-complete="() => {}"
:link-auto-complete="false"
Expand Down Expand Up @@ -50,7 +58,9 @@ import { generateOcsUrl } from '@nextcloud/router'
import axios from '@nextcloud/axios'
import { showError } from '@nextcloud/dialogs'
import { loadState } from '@nextcloud/initial-state'
import { MAX_TEXT_INPUT_LENGTH } from '../../constants.js'
import { getCurrentUser } from '@nextcloud/auth'
import { uploadInputFile } from '../../utils.js'
import { VALID_TEXT_MIME_TYPES, MAX_TEXT_INPUT_LENGTH } from '../../constants.js'

/*
maxlength calculation (just a rough estimate):
Expand Down Expand Up @@ -105,6 +115,7 @@ export default {
thinkingText: t('assistant', 'Processing…'),
submitBtnAriaText: t('assistant', 'Submit'),
isRecording: false,
isDraggedOver: false,
audioChatAvailable: loadState('assistant', 'audio_chat_available', false),
}
},
Expand Down Expand Up @@ -148,6 +159,88 @@ export default {
this.$emit('submit', e)
}
},
onDragOver(e) {
const fileItems = [...e.dataTransfer.items].filter(
(item) => item.kind === 'file',
)
if (fileItems.length === 1) {
e.preventDefault()
e.stopPropagation()
this.isDraggedOver = true
}
},
onDragEnter(e) {
const fileItems = [...e.dataTransfer.items].filter(
(item) => item.kind === 'file',
)
if (fileItems.length === 1) {
e.preventDefault()
e.stopPropagation()
this.isDraggedOver = true
}
},
onDragLeave(e) {
const fileItems = [...e.dataTransfer.items].filter(
(item) => item.kind === 'file',
)
if (fileItems.length === 1) {
e.preventDefault()
e.stopPropagation()
this.isDraggedOver = false
}
},
onDrop(e) {
e.preventDefault()
e.stopPropagation()
const fileItems = [...e.dataTransfer.items].filter(
(item) => item.kind === 'file',
)
if (fileItems.length === 1) {
this.uploadDroppedFile(fileItems[0].getAsFile())
this.isDraggedOver = false
}
},
uploadDroppedFile(file) {
if (!VALID_TEXT_MIME_TYPES.includes(file.type)) {
showError(t('assistant', 'Invalid file type. Only text files are supported.'))
return
}
this.isUploading = true

return uploadInputFile(file)
.then((response) => {
const data = response.data.ocs.data
this.onFileUploaded({ fileId: data.fileId, filePath: data.filePath })
})
.catch(error => {
console.error('error while uploading a file after drop', error)
})
.then(() => {
this.isUploading = false
})
},
onFileUploaded({ fileId, filePath }) {
const userId = getCurrentUser()?.uid
if (!userId) {
return
}
const userFilePath = filePath.replace('/' + userId + '/files/', '/')
const url = generateOcsUrl('/apps/assistant/api/v1/parse-file')
axios.post(url, {
filePath: userFilePath,
}).then((response) => {
const data = response.data?.ocs?.data
if (data?.parsedText === undefined) {
showError(t('assistant', 'Unexpected response from text parser'))
return
}

this.$emit('update:chatContent', data?.parsedText)
}).catch((error) => {
console.error(error)
showError(t('assistant', 'Could not parse file'))
})
},
},
}
</script>
Expand Down Expand Up @@ -184,6 +277,11 @@ export default {
align-items: end;
gap: 4px;

&.draggedOver .input {
border: solid 2px var(--color-border-success);
border-radius: var(--border-radius-large);
}

:deep(&__thinking > div) {
font-style: italic;
animation: breathing 2s linear infinite normal;
Expand Down
99 changes: 98 additions & 1 deletion src/components/fields/TextInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="text-input">
<div class="text-input"
:class="{
draggedOver: isDraggedOver,
}"
@dragover="onDragOver"
@dragenter="onDragEnter"
@dragleave="onDragLeave"
@drop="onDrop">
<label :for="id">
{{ label }}
<br v-if="limitLabel">
Expand Down Expand Up @@ -58,6 +65,8 @@ import isMobile from '../../mixins/isMobile.js'
import axios from '@nextcloud/axios'
import { getFilePickerBuilder, showError } from '@nextcloud/dialogs'
import { generateOcsUrl } from '@nextcloud/router'
import { getCurrentUser } from '@nextcloud/auth'
import { uploadInputFile } from '../../utils.js'
import { VALID_TEXT_MIME_TYPES, MAX_TEXT_INPUT_LENGTH } from '../../constants.js'

const picker = (callback, target) => getFilePickerBuilder(t('assistant', 'Choose a text file'))
Expand Down Expand Up @@ -127,6 +136,7 @@ export default {
return {
copied: false,
maxLength: MAX_TEXT_INPUT_LENGTH,
isDraggedOver: false,
}
},

Expand Down Expand Up @@ -194,6 +204,88 @@ export default {
showError(t('assistant', 'Result could not be copied to clipboard'))
}
},
onDragOver(e) {
const fileItems = [...e.dataTransfer.items].filter(
(item) => item.kind === 'file',
)
if (fileItems.length === 1) {
e.preventDefault()
e.stopPropagation()
this.isDraggedOver = true
}
},
onDragEnter(e) {
const fileItems = [...e.dataTransfer.items].filter(
(item) => item.kind === 'file',
)
if (fileItems.length === 1) {
e.preventDefault()
e.stopPropagation()
this.isDraggedOver = true
}
},
onDragLeave(e) {
const fileItems = [...e.dataTransfer.items].filter(
(item) => item.kind === 'file',
)
if (fileItems.length === 1) {
e.preventDefault()
e.stopPropagation()
this.isDraggedOver = false
}
},
onDrop(e) {
e.preventDefault()
e.stopPropagation()
const fileItems = [...e.dataTransfer.items].filter(
(item) => item.kind === 'file',
)
if (fileItems.length === 1) {
this.uploadDroppedFile(fileItems[0].getAsFile())
this.isDraggedOver = false
}
},
uploadDroppedFile(file) {
if (!VALID_TEXT_MIME_TYPES.includes(file.type)) {
showError(t('assistant', 'Invalid file type. Only text files are supported.'))
return
}
this.isUploading = true

return uploadInputFile(file)
.then((response) => {
const data = response.data.ocs.data
this.onFileUploaded({ fileId: data.fileId, filePath: data.filePath })
})
.catch(error => {
console.error('error while uploading a file after drop', error)
})
.then(() => {
this.isUploading = false
})
},
onFileUploaded({ fileId, filePath }) {
const userId = getCurrentUser()?.uid
if (!userId) {
return
}
const userFilePath = filePath.replace('/' + userId + '/files/', '/')
const url = generateOcsUrl('/apps/assistant/api/v1/parse-file')
axios.post(url, {
filePath: userFilePath,
}).then((response) => {
const data = response.data?.ocs?.data
if (data?.parsedText === undefined) {
showError(t('assistant', 'Unexpected response from text parser'))
return
}

this.$emit('update:value', data?.parsedText)
}).catch((error) => {
console.error(error)
showError(t('assistant', 'Could not parse file'))
})
},
},
}
</script>
Expand All @@ -212,6 +304,11 @@ body[dir="rtl"] .choose-file-button {
.text-input {
position: relative;

&.draggedOver {
border: solid 2px var(--color-border-success);
border-radius: var(--border-radius-large);
}

.copy-button,
.choose-file-button {
position: absolute !important;
Expand Down
Loading