обновОЧКИ
This commit is contained in:
@@ -16,15 +16,7 @@ export function qChatMessages() {
|
||||
const response = await api.chad.chatMessages({ cursor: pageParam, limit: 25 })
|
||||
|
||||
return response.data
|
||||
// return {
|
||||
// messages: response.data.messages.reverse(),
|
||||
// nextCursor: response.data.nextCursor,
|
||||
// }
|
||||
},
|
||||
// select: data => ({
|
||||
// pages: [...data.pages].reverse(),
|
||||
// pageParams: [...data.pageParams].reverse(),
|
||||
// }),
|
||||
select: (data) => {
|
||||
return data.pages.flatMap(page => page.messages).toReversed()
|
||||
},
|
||||
|
||||
62
new-client/src/widgets/chat/ui/ChatAttachment.vue
Normal file
62
new-client/src/widgets/chat/ui/ChatAttachment.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
bem.b(),
|
||||
bem.is('loading', loading),
|
||||
bem.is('error', error),
|
||||
]"
|
||||
>
|
||||
{{ name }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useBem from '@shared/composables/use-bem.ts'
|
||||
|
||||
const props = defineProps<{
|
||||
name: string
|
||||
loading?: boolean
|
||||
error?: boolean
|
||||
}>()
|
||||
|
||||
const bem = useBem('chat-attachment')
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.chat-attachment {
|
||||
--stripe-width: 20px;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: var(--space-1) var(--space-2);
|
||||
background-color: var(--grey-1);
|
||||
outline: 1px solid var(--ink);
|
||||
outline-offset: -1px;
|
||||
cursor: pointer;
|
||||
|
||||
background-repeat: repeat;
|
||||
color: var(--grey-3);
|
||||
|
||||
&.is-loading {
|
||||
background-image: repeating-linear-gradient(
|
||||
-45deg,
|
||||
transparent 25%,
|
||||
transparent 50%,
|
||||
var(--grey-2) 50%,
|
||||
var(--grey-2) 75%
|
||||
);
|
||||
background-size: var(--stripe-width) var(--stripe-width);
|
||||
animation: loading 500ms infinite linear;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
from {
|
||||
background-position-x: 0;
|
||||
}
|
||||
to {
|
||||
background-position-x: var(--stripe-width);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -17,39 +17,50 @@
|
||||
|
||||
<ChadButton
|
||||
class="chat-input__send"
|
||||
:loading="isUploading"
|
||||
@click="sendMessage()"
|
||||
>
|
||||
Send
|
||||
</ChadButton>
|
||||
</div>
|
||||
|
||||
<ul v-if="fileUploadApi.acceptedFiles.length > 0" class="chat-input__attachments" v-bind="fileUploadApi.getItemGroupProps()">
|
||||
<li
|
||||
<div v-if="fileUploadApi.acceptedFiles.length > 0" class="chat-input__attachments" v-bind="fileUploadApi.getItemGroupProps()">
|
||||
<ChatAttachment
|
||||
v-for="file in fileUploadApi.acceptedFiles"
|
||||
:key="file.name"
|
||||
:loading="uploadStates.get(file)?.status === 'loading'"
|
||||
:error="uploadStates.get(file)?.status === 'error'"
|
||||
class="chat-input__attachment"
|
||||
v-bind="fileUploadApi.getItemProps({ file })"
|
||||
@click="fileUploadApi.deleteFile(file)"
|
||||
>
|
||||
{{ file.name }}
|
||||
</li>
|
||||
</ul>
|
||||
:name="file.name"
|
||||
@click="removeFile(file)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input v-bind="fileUploadApi.getHiddenInputProps()">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { NewChatMessagePayload } from '@shared/api/generated-chad-api.ts'
|
||||
import type { FullRequestParams, NewChatMessagePayload } from '@shared/api/generated-chad-api.ts'
|
||||
import { Plus } from '@lucide/vue'
|
||||
import api from '@shared/api/client.ts'
|
||||
import { ContentType } from '@shared/api/generated-chad-api.ts'
|
||||
import ChadButton from '@shared/components/ui/Button.vue'
|
||||
import ChadInput from '@shared/components/ui/Input.vue'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { useChat } from '@widgets/chat/composables/use-chat.ts'
|
||||
import ChatAttachment from '@widgets/chat/ui/ChatAttachment.vue'
|
||||
import * as fileUpload from '@zag-js/file-upload'
|
||||
import { normalizeProps, useMachine } from '@zag-js/vue'
|
||||
import { computed, reactive, useId } from 'vue'
|
||||
|
||||
interface UploadStatus {
|
||||
status: 'loading' | 'done' | 'error'
|
||||
uuid?: string
|
||||
abortController: AbortController
|
||||
}
|
||||
|
||||
const id = useId()
|
||||
const chat = useChat()
|
||||
|
||||
@@ -58,22 +69,46 @@ const message: NewChatMessagePayload = reactive({
|
||||
attachments: [],
|
||||
})
|
||||
|
||||
const uploadStates = reactive(new Map<File, UploadStatus>())
|
||||
|
||||
const isUploading = computed(() => {
|
||||
return Array.from(uploadStates.values()).some(s => s.status === 'loading')
|
||||
})
|
||||
|
||||
const service = useMachine(fileUpload.machine, {
|
||||
id,
|
||||
maxFiles: 10,
|
||||
maxFileSize: 100 * 1024 * 1024,
|
||||
maxFileSize: 1000 * 1024 * 1024,
|
||||
name: 'chat-input',
|
||||
capture: 'environment',
|
||||
allowDrop: false,
|
||||
onFileAccept: (details) => {
|
||||
console.log('onFileAccept', details)
|
||||
},
|
||||
// transformFiles: async (files) => {
|
||||
// console.log(files)
|
||||
//
|
||||
// return files
|
||||
// },
|
||||
for (const file of details.files) {
|
||||
if (uploadStates.has(file))
|
||||
continue
|
||||
|
||||
const state: UploadStatus = { status: 'loading', abortController: new AbortController() }
|
||||
uploadStates.set(file, state)
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
try {
|
||||
api.chad.attachmentUpload({
|
||||
body: formData,
|
||||
format: 'text',
|
||||
type: ContentType.FormData,
|
||||
signal: state.abortController.signal,
|
||||
} as FullRequestParams).then((response) => {
|
||||
uploadStates.set(file, { ...state, status: 'done', uuid: response.data })
|
||||
message.attachments!.push(response.data)
|
||||
})
|
||||
}
|
||||
catch {
|
||||
uploadStates.set(file, { ...state, status: 'error' })
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const fileUploadApi = computed(() => fileUpload.connect(service, normalizeProps))
|
||||
@@ -82,7 +117,16 @@ useEventListener(document, 'paste', (event) => {
|
||||
fileUploadApi.value.setClipboardFiles(event.clipboardData)
|
||||
})
|
||||
|
||||
function removeFile(file: File) {
|
||||
uploadStates.get(file)?.abortController.abort('Attachment removed')
|
||||
uploadStates.delete(file)
|
||||
fileUploadApi.value.deleteFile(file)
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
if (isUploading.value)
|
||||
return
|
||||
|
||||
chat.sendMessage(message)
|
||||
|
||||
reset()
|
||||
@@ -91,6 +135,8 @@ function sendMessage() {
|
||||
function reset() {
|
||||
message.text = ''
|
||||
message.attachments = []
|
||||
uploadStates.clear()
|
||||
fileUploadApi.value.clearFiles()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -125,14 +171,11 @@ function reset() {
|
||||
margin-top: var(--space-2);
|
||||
outline: var(--border-w) dashed var(--ink);
|
||||
outline-offset: calc(var(--border-w) * -1);
|
||||
}
|
||||
overflow: hidden;
|
||||
|
||||
&__attachment {
|
||||
padding: var(--space-1) var(--space-2);
|
||||
background-color: var(--grey-1);
|
||||
outline: 1px solid var(--ink);
|
||||
outline-offset: -1px;
|
||||
cursor: pointer;
|
||||
> * {
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="chat-message-attachment" @click="download">
|
||||
<div class="chat-message-attachment" :title="attachment.name" @click="download">
|
||||
<div class="chat-message-attachment__extension">
|
||||
{{ extension }}
|
||||
</div>
|
||||
@@ -8,7 +8,7 @@
|
||||
{{ attachment.name }}
|
||||
</p>
|
||||
<p class="chat-message-attachment__size">
|
||||
{{ attachment.size }} KB
|
||||
{{ size }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -18,18 +18,23 @@
|
||||
import type { Attachment } from '@shared/api/generated-chad-api.ts'
|
||||
import api from '@shared/api/client.ts'
|
||||
import { downloadFile } from '@zag-js/file-utils'
|
||||
import { formatBytes } from '@zag-js/i18n-utils'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
attachment: Attachment
|
||||
}>()
|
||||
|
||||
const extension = computed(() => props.attachment.name.split('.')[1])
|
||||
const extension = computed(() => props.attachment.name.split('.').at(-1))
|
||||
|
||||
const url = computed(() => {
|
||||
return `${__API_BASE_URL__}/attachment/${props.attachment.id}`
|
||||
})
|
||||
|
||||
const size = computed(() => {
|
||||
return formatBytes(props.attachment.size, 'en-US', { unitSystem: 'binary' })
|
||||
})
|
||||
|
||||
async function download() {
|
||||
const response = await api.chad.attachmentGet(props.attachment.id, { format: 'blob' })
|
||||
|
||||
@@ -46,7 +51,7 @@ async function download() {
|
||||
background-color: var(--grey-1);
|
||||
width: 300px;
|
||||
height: 52px;
|
||||
//overflow: hidden;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
@@ -68,6 +73,7 @@ async function download() {
|
||||
flex: 1;
|
||||
padding: var(--space-3);
|
||||
align-self: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__name {
|
||||
|
||||
@@ -26,6 +26,8 @@ const src = computed(() => {
|
||||
height: 160px;
|
||||
|
||||
> img {
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
max-width: 260px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user