mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-27 09:08:30 +00:00
51ed038374
Threads carry a journey scope next to the trip scope, with journey rights (outsider 404, viewers without write tools, addon off), its own routes and a book context, and the client keys chats by trip or journey.
89 lines
2.9 KiB
TypeScript
89 lines
2.9 KiB
TypeScript
import { LLM_PHOTO_EXTENSIONS } from '@trek/shared';
|
|
import { type DragEvent, useState } from 'react';
|
|
|
|
import { useTranslation } from '../../i18n';
|
|
import { assistantScopeOf } from '../../store/assistantChatKey';
|
|
import { chatOf, useAssistantStore } from '../../store/assistantStore';
|
|
import { useToast } from '../shared/Toast';
|
|
|
|
/** What the booking import reads (the server checks again), and how big one file may be. */
|
|
const ATTACH_EXTENSIONS: readonly string[] = [
|
|
'.pdf',
|
|
'.eml',
|
|
'.pkpass',
|
|
'.ics',
|
|
'.html',
|
|
'.htm',
|
|
'.txt',
|
|
...LLM_PHOTO_EXTENSIONS,
|
|
];
|
|
const MAX_ATTACH_BYTES = 10 * 1024 * 1024;
|
|
|
|
function extensionOf(name: string): string {
|
|
const dot = name.lastIndexOf('.');
|
|
return dot < 0 ? '' : name.slice(dot).toLowerCase();
|
|
}
|
|
|
|
function hasFiles(e: DragEvent): boolean {
|
|
return Array.from(e.dataTransfer.types).includes('Files');
|
|
}
|
|
|
|
/**
|
|
* Hands a booking file to the chat, from the paperclip, a paste or a drop on
|
|
* the window. One file at a time, refused here with a toast when it is not
|
|
* one the import reads or too big, so a wrong file never costs a round trip.
|
|
* The drop handlers go on the whole chat; `dragging` lights its drop zone.
|
|
* A journey's chat imports no bookings: there `canAttach` is false, a file is
|
|
* left to the browser and the drop zone never lights.
|
|
*/
|
|
export function useAssistantAttach(tripId: number) {
|
|
const { t } = useTranslation();
|
|
const toast = useToast();
|
|
const attach = useAssistantStore((s) => s.attach);
|
|
const busy = useAssistantStore((s) => chatOf(s, tripId).isRunning);
|
|
const [dragging, setDragging] = useState(false);
|
|
const canAttach = assistantScopeOf(tripId).kind === 'trip';
|
|
|
|
const take = (files: FileList | File[] | null | undefined): boolean => {
|
|
const file = files ? Array.from(files)[0] : undefined;
|
|
if (!file || !canAttach) return false;
|
|
if (busy) return true;
|
|
if (!ATTACH_EXTENSIONS.includes(extensionOf(file.name))) {
|
|
toast.warning(t('assistant.attach.unsupported'));
|
|
return true;
|
|
}
|
|
if (file.size > MAX_ATTACH_BYTES) {
|
|
toast.warning(t('assistant.attach.tooLarge'));
|
|
return true;
|
|
}
|
|
void attach(tripId, file);
|
|
return true;
|
|
};
|
|
|
|
const dropZone = {
|
|
onDragEnter: (e: DragEvent) => {
|
|
if (!canAttach || !hasFiles(e)) return;
|
|
e.preventDefault();
|
|
setDragging(true);
|
|
},
|
|
onDragOver: (e: DragEvent) => {
|
|
if (!canAttach || !hasFiles(e)) return;
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = 'copy';
|
|
},
|
|
onDragLeave: (e: DragEvent) => {
|
|
// Leaving for a child is not leaving the zone.
|
|
if (e.currentTarget.contains(e.relatedTarget as Node | null)) return;
|
|
setDragging(false);
|
|
},
|
|
onDrop: (e: DragEvent) => {
|
|
if (!canAttach || !hasFiles(e)) return;
|
|
e.preventDefault();
|
|
setDragging(false);
|
|
take(e.dataTransfer.files);
|
|
},
|
|
};
|
|
|
|
return { take, busy, canAttach, dragging, dropZone, accept: ATTACH_EXTENSIONS.join(',') };
|
|
}
|