обновОЧКИ

This commit is contained in:
2026-05-29 04:28:09 +06:00
parent 3c885edc46
commit 0dd9efb9fb
15 changed files with 542 additions and 44 deletions

View File

@@ -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()
},

View 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>

View File

@@ -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>

View File

@@ -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 {

View File

@@ -26,6 +26,8 @@ const src = computed(() => {
height: 160px;
> img {
object-fit: contain;
object-position: center;
max-width: 260px;
height: 100%;
}