mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 07:26:30 +00:00
8a8ce4013b
- Tests that still knew the ChatGPT sign-in as personal only follow the admin's shared plan: the service takes the caller with their role, the admin tiles show ChatGPT, the controller tests cover the admin routes. - The table refused an instance ChatGPT connection with a CHECK, so sharing the plan failed. A new migration rebuilds ai_connections without that check; the shipped migration stays as it was. - useChatgptConnect grew past the function size limit: reading the clipboard and noticing the return to the tab are helpers of their own. - The file link factory hands its overrides over apart from the row, like every other factory, which ends the deep type instantiation. - Knip: exports only their own file used are private again, and the booking cap reads MAX_WRITES_PER_STEP instead of an alias. - lint:i18n-keys reaches the tools' own action names through a key builder and the connection hints through literal keys. - MMehrSheet stays in the assistant hints pair only.
85 lines
2.6 KiB
TypeScript
85 lines
2.6 KiB
TypeScript
import { LLM_PHOTO_EXTENSIONS } from '@trek/shared';
|
|
import { type DragEvent, useState } from 'react';
|
|
|
|
import { useTranslation } from '../../i18n';
|
|
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.
|
|
*/
|
|
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 take = (files: FileList | File[] | null | undefined): boolean => {
|
|
const file = files ? Array.from(files)[0] : undefined;
|
|
if (!file) 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 (!hasFiles(e)) return;
|
|
e.preventDefault();
|
|
setDragging(true);
|
|
},
|
|
onDragOver: (e: DragEvent) => {
|
|
if (!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 (!hasFiles(e)) return;
|
|
e.preventDefault();
|
|
setDragging(false);
|
|
take(e.dataTransfer.files);
|
|
},
|
|
};
|
|
|
|
return { take, busy, dragging, dropZone, accept: ATTACH_EXTENSIONS.join(',') };
|
|
}
|