Repository navigation
Attach files to Research AI chat messages - #1143
Merged
Merged
Conversation
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
This was referenced Oct 6, 2026
PNG, JPEG, GIF and WebP join the types the composer takes, mirroring the backend's table in researchhub-backend#4241. The refusal for any other type uses the server's new wording.
A browser's "Copy image" puts a bare <img> tag on the clipboard beside the image. HTML with no text in it no longer makes the paste a text paste, so the image is attached. HTML that holds text, and any RTF, still leave the paste to the browser.
GET files/limits/ (researchhub-backend#4242) now drives the checks before upload, the picker's accepted types, the refusal sentences and the kind shown on a chip. The backend's defaults stay in force until the response arrives and on a server without the route. The refusal of an empty file takes the server's sentence.
With at least one file attached and all of them ready, Send no longer needs text (researchhub-backend#4243), and a message without text draws no bubble. Offered only once the server's limits have been read: the two reached the backend together, and an older server refuses a blank message.
When an upload is refused because too many files are waiting to be sent, the composer reads GET files/ (researchhub-backend#4242) and offers to remove those outside the open composer. Nothing is deleted without that click, and the refused uploads then start by themselves. The list is not read on a server that has not published its limits: there the route takes POST only, and a GET would spend the upload throttle.
|
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
Lets users attach uploaded files to Research AI chat messages, in the notebook's assistant panel and in the assistant chat (AI Mode). Both surfaces get it through the code they already share: the composer, the transcript,
useAgentChatand the chat transport.Depends on ResearchHub/researchhub-backend#4229 (merged) and ResearchHub/researchhub-backend#4231 (merged). Reads one optional field,
pages_without_text, from ResearchHub/researchhub-backend#4235 (merged) and works without it. Images need ResearchHub/researchhub-backend#4241 (merged on Oct 6) deployed: until it is, the server refuses an image withunsupported_file_type. Uses ResearchHub/researchhub-backend#4242 (merged: the upload limits, the list of unsent files, the code for an empty file) and ResearchHub/researchhub-backend#4243 (merged: a message of files alone), and works without them: see Before the backend deploy under Notes.AgentFileServicecovers the upload flow: create, send the bytes straight to the bucket's presigned form (with progress), complete, read status, delete, and fetch a download URL. BothsendMessagecalls takefile_ids, and messages carryattachments.GET files/limits/(25 MB, 5 files a message and 20 a chat by default). Server refusals are still shown as given: thedetailof a refused upload or message, the 503 when uploads are not configured, and a throttled upload with how long to wait.too_many_unsent_files), the composer readsGET files/and says how many wait in other chats or tabs, with an action to remove them. Nothing is deleted without that click. See Files the page has no record of under Notes..png,.jpg,.jpeg,.gif,.webp) are accepted through the same flow and limits as documents, from the picker, a drop or a paste. The chip and the sent message show an image's kind alone ("PNG image"): it has no page count and no scan note. HEIC, TIFF, BMP and SVG are refused, as on the server. A file that is not really an image, or is too large to decode, fails with the server's reason like any unreadable file. No thumbnail is shown; an image opens in a new tab like any other file.pages_without_text, the chip and the sent message say so: "No readable text found", or how many pages have none. This is a note about the file. It does not depend on the chat's model, and it neither warns nor blocks sending.notes.md, no longer renders the name as a link. The chat renderer (components/AgentChat/MarkdownMessage.tsx) linked any schemelessname.tldwhose ending is a known domain, and that list has every two-letter country code, sonotes.md,analysis.py,run.sh,main.rsandtodo.orgall became links. Schemeless text is now linked only when it starts withwww.. Still linked: anything with a scheme,www.links, explicit markdown links,<https://…>autolinks and email addresses.Notes
sessionStorageand re-read from the server on load. The server's list of unsent files is per user, not per chat or tab, so it cannot say which chat a file was attached in;sessionStoragestays the record of that.sessionStoragerather thanlocalStorageso two tabs do not act on each other's uploads.FAILED, which holds no slot. That includes files waiting in this tab's other chats, which leave their composers. After the click the refused uploads start again by themselves. If the removal failed they are refused again and the offer returns. The list is read only at that moment, never on load.GET files/limits/is read once per page load and drives the checks before upload, the picker's accepted types, the numbers and the extension list in the refusal sentences, and the kind shown on a chip, so a deployment override or a new file type needs no client change. The backend's defaults (DEFAULT_AGENT_FILE_LIMITSintypes/agentFile.ts) apply until the response arrives and whenever it cannot be read. Two phrases stay fixed in the client, as the first is on the server: the lead-in of the unsupported-type sentence ("a PDF, Word (.docx), text, or image file") and the paperclip's tooltip. A new family of file types would need both reworded.GET files/is never called, so a refused upload reads as the server worded it, with no offer. The list is held back on purpose: on the older backendfiles/takesPOSTonly and its upload throttle covers the whole view, so aGETthere would spend one of the user's 60 uploads an hour before being answered with a 405. A 404 from the limits route is not asked again until a reload. Any other failure is asked again when the composer moves to another chat.EntityTooLarge) now reads "This file is too large to upload." It quoted the 25 MB constant. The checks before it use the published limit, so it is reached only when a file's bytes exceed the size it declared.POSTfrom the web origins, which the upload needs: it is an XHR with progress events, and browsers preflight those. Preflights sent on Oct 6 pass on prod forresearchhub.comandwww.researchhub.com, and on staging forstaging.researchhub.comandexperiment.staging.researchhub.com(each with and withoutwww.) and forlocalhost:3000.file:URL); there the files attach and the name is not typed into the box. A pasted screenshot is an image alone, so it is attached. HTML counts as text only when it holds some: a browser's "Copy image" puts a bare<img>tag on the clipboard beside the image, and that image is attached too.example.comorresearchhub.com/paper/1written without a scheme orwww.stay plain text, since they cannot be told apart from filenames by shape (todo.orgagainstnature.org).MarkdownMessageis the only user of markdown-it in the app. It renders assistant answers and the narration, thinking and tool-draft text in the activity feed, on both chat surfaces. Nothing outside the agent chat uses it.Test plan
npm run type-checknpm run lint(0 errors; the 2 warnings are in files this PR does not touch and are onmain)prettier --checkon the changed filesnpm run buildfile_idson the request; attachments on the pending and the recorded message; the three activity rows; opening a file and a refused download; a failed file and removing it by keyboard; removing mid-upload; drag and drop on the pane; files staying with their chat across a switch, a note change and a reload; 503, 429 and bucket refusals; a message refused over an expired file; the scanned-PDF note staying the same on a text-only model; the notebook panel docked and as a phone sheetnotes.md,analysis.py,run.sh,main.rs,lib.cc,todo.org,project.proand the bare domainsexample.com,claude.ai,researchhub.com/paper/1are plain text;https://example.com/notes.md,http://example.org,www.example.com,www.site.de/path, a markdown link, an autolink and an email address are links. The same answer on the unfixed renderer linked all seventeensmokefails on one test,auth.spec.ts:68(register a new account), which fails the same way on other open PRs.JPG, a.jpeg, a GIF and a WebP picked together, each sent to the bucket with its image content type and shown as its kind and size; a dropped JPEG; a pasted screenshot (image.png, an image alone); an image file pasted with its own name as text; HEIC, TIFF, BMP and SVG refused with the new sentence and no request made; a text file namedfake.pngending as failed with the server's reason and blocking send until removed; a message sent with two images (file_idson the request, both files on the pending and the recorded message, kind alone); opening a sent image through the download endpoint; an unsent image coming back after a reload; the stub switched to the old table, where the chip shows the server's refusal<img>tag is attached, in the two shapes Chrome writes for "Copy image" on macOS and on Windows (written by hand, not captured from a real copy); an image beside Word-style text, HTML and RTF, beside HTML that holds text, or beside unrelated plain text is not, and the paste reaches the browser unpreventedmain. In AI Mode: limits read once and used for the picker's types and size; a stub deployment with 2 MB, 2 files a message, 3 a chat and an extra.pptxtype, where the picker, the refusal sentences, the chip's kind and the limits all follow the response; an empty file refused with the server's sentence and no request; files alone sent on a new chat and with Enter on an open one (message: ""andfile_idson the request, no text bubble on the pending or the recorded message, the chat titled from the file name); Send off with nothing to send, with whitespace alone, and with a failed file. The unsent cap, with 18 files seeded as another tab's, one failed file and one file in another chat of the same tab: the refused chip and "19 files are waiting", nothing deleted until the click, the offer withdrawn when the refused file is removed, then exactly those 19 deleted, the refused file uploaded, and the other chat's composer emptied; a failed removal bringing the refusal and the offer back; a failed list leaving the refusal alone; the singular wording. In the notebook panel: files alone on a new chat and on the open one, the picker row of such a chat, and the cap offer in the docked panelGET files/405, a blank message refused). In AI Mode: the defaults in force, a ready file without text not sendable by button or Enter and no message posted, a message with text sent as before, the cap refusal as the server worded it with no offer, limits asked for once and not again on a chat switch,GET files/never called. In the notebook panel: the same send gate and the same two request counts. With the limits request failing (500) or malformed, in AI Mode: the defaults in force and no send of files alone; once the request succeeds on the next chat, files alone sendfiles/limits/answered 404 without a session and now answers 401, andfiles/now allowsGET