Skip to content

feat: improved file upload frontend and add attachment validation#981

Open
puzzle-solver wants to merge 6 commits into
pk/file-uploadfrom
pk/file-upload-frontend-and-validation
Open

feat: improved file upload frontend and add attachment validation#981
puzzle-solver wants to merge 6 commits into
pk/file-uploadfrom
pk/file-upload-frontend-and-validation

Conversation

@puzzle-solver

Copy link
Copy Markdown
Collaborator

Added additional frontend+backend validation of uploads (configurable from the backend). Improved UI showing the attached files.

image

@dazy-ds dazy-ds left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good, I would like to have some additional context before approving. I just want to fully understand the rationale behind the implementation C:

Also left some food for thoughts :D

Comment on lines +1 to +16
export type FileDropHandler = (files: File[]) => void;

let handler: FileDropHandler | null = null;

export function registerFileDropHandler(h: FileDropHandler): () => void {
handler = h;
return () => {
if (handler === h) handler = null;
};
}

export function dispatchDroppedFiles(files: File[]): boolean {
if (!handler || files.length === 0) return false;
handler(files);
return true;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume that this was added as a way of allowing plugins to register for file events, right?

If so, this is kind of a big architecture change/new feature and I think it should be discussed.

The implementation of this seems fine, as it somewhat mimics how we handle slots, etc. but I would like to know rationale behind that implementation.

On the side note, if we decide to keep it, I would move it to a directory called registires or events or something similar. That way we can communicate that those methods are used to register some handlers of global events emitted by the UI (just like in an even driven application, e.g. CMSes have similar mechanisms).

Comment on lines +1 to +36
export interface PreSendContribution {
context?: Record<string, unknown>;
userMessageExtra?: Record<string, unknown>;
files?: File[];
}

type Contributor = () => PreSendContribution | null;

const contributors = new Set<Contributor>();

export function registerPreSendContributor(c: Contributor): () => void {
contributors.add(c);
return () => {
contributors.delete(c);
};
}

export function collectPreSendContext(): PreSendContribution {
let context: Record<string, unknown> | undefined;
let userMessageExtra: Record<string, unknown> | undefined;
const files: File[] = [];
for (const c of contributors) {
const r = c();
if (!r) continue;
if (r.context) context = { ...(context ?? {}), ...r.context };
if (r.userMessageExtra) {
userMessageExtra = { ...(userMessageExtra ?? {}), ...r.userMessageExtra };
}
if (r.files && r.files.length > 0) files.push(...r.files);
}
return {
context,
userMessageExtra,
files: files.length > 0 ? files : undefined,
};
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

Comment on lines +65 to +88
add: (file: File) => {
const id = crypto.randomUUID();
const entry: UploadingAttachment = {
id,
filename: file.name,
mimeType: file.type || "application/octet-stream",
blobUrl: URL.createObjectURL(file),
status: "uploading",
};
set((state) => ({ pending: [...state.pending, entry] }));
return id;
},
addFailed: (file: File, error: string, retryFile?: File) => {
const entry: FailedAttachment = {
id: crypto.randomUUID(),
filename: file.name,
mimeType: file.type || "application/octet-stream",
blobUrl: URL.createObjectURL(file),
status: "failed",
error,
retryFile,
};
set((state) => ({ pending: [...state.pending, entry] }));
},

@dazy-ds dazy-ds May 18, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it could be merged into one method. We could shift the setting of "status" to consumer of the store and expect objects of a specified structure as arguments (to enforce the required arguments).

E.g.

interface FailedFile {
  file: File,
  status: "failed",
  error: string,
  retryFile?: File
}

interface UploadingFile {
  file: File,
  status: "uploading"
}
add(fileObject: UploadingFile | FailedFile) {}

I want to note that this is a pure cosmetic change coming from my preference :D

We can also keep this as is, but abstract the common entry creation process (it would give the id, extract filename, etc), and keep the case specific code in the existing methods.

Comment on lines +32 to +44
unregisterContributor = registerPreSendContributor(() => {
const { files, previews } = useUploadAttachmentsStore
.getState()
.consumeReady();
if (files.length === 0) return null;
return {
files,
userMessageExtra: { attachments: previews },
};
});
unregisterDropHandler = registerFileDropHandler((files) => {
files.forEach(processFile);
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registering this way is okay (although I am not a fan of usage of global variables for such purposes).

We can also consider expanding the Plugin to support a list of callbacks to be returned from onActivate that would be automatically called when plugin is deactivated. It can simplify the plugin registration process while also keeping onDeactivate for some general stuff.

Either way I am fine with that approach, just my thought.

}
requestBody = formData;
attachmentsStore.getState().clear();
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the part that got most of my attention in this PR.

Previous version negated whole purpose of having plugins, because it directly imported a file from a plugin into one of the core entities. The fixed version is better, but I am still not sure about splitting files and userMessageExtra objects.

One can also wonder why would we add a utility to add files into the core, other that for uploading them. I can't see other use case for files dropped into the chat, other than them being uploaded to be processed (or processed on the client). Having that in mind, we could consider adding whole UploadPlugin into the core.

To summarize I think this is fine with a small nit about naming and separating files from userMessageExtra, because for me, files are extra additions to user's message.

}
case "wrong-type":
return `File type not supported. Allowed: ${error.allowed.join(", ")}.`;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: We can add exhaustive check to always make sure that we have all cases handled in case a new kind is added (https://stackoverflow.com/questions/39419170/how-do-i-check-that-a-switch-block-is-exhaustive-in-typescript)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants