mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-15 22:14:46 +00:00
fix: handle non text uploads more carefully
This commit is contained in:
1 parent
82691314ca
commit
3030aba39d
21 files changed
+729
-40
No files matched your search
@@ -28,6 +28,7 @@ import {
|
||||
import { ActiveState, Doc } from '../models/misc';
|
||||
import {
|
||||
selectSelectedDocs,
|
||||
selectSelectedModel,
|
||||
selectSourceDocs,
|
||||
selectToken,
|
||||
setSelectedDocs,
|
||||
@@ -47,6 +48,7 @@ import {
|
||||
ToolsTrigger,
|
||||
} from './message-input';
|
||||
import { useArmedSend } from './message-input/armedSend';
|
||||
import { cannotReadAttachment } from './message-input/attachmentReadability';
|
||||
import { handleAbort } from '../conversation/conversationSlice';
|
||||
import {
|
||||
AUDIO_FILE_ACCEPT_ATTR,
|
||||
@@ -345,6 +347,21 @@ export default function MessageInput({
|
||||
const sourceDocs = useSelector(selectSourceDocs);
|
||||
const token = useSelector(selectToken);
|
||||
const attachments = useSelector(selectAttachments);
|
||||
const selectedModel = useSelector(selectSelectedModel);
|
||||
const unreadableAttachmentIds = useMemo(
|
||||
() =>
|
||||
new Set(
|
||||
attachments
|
||||
.filter((attachment) =>
|
||||
cannotReadAttachment(
|
||||
attachment,
|
||||
selectedModel?.supported_attachment_types,
|
||||
),
|
||||
)
|
||||
.map((attachment) => attachment.id),
|
||||
),
|
||||
[attachments, selectedModel],
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const store = useStore<RootState>();
|
||||
@@ -453,6 +470,12 @@ export default function MessageInput({
|
||||
...(Number.isFinite(tokenCount)
|
||||
? { token_count: tokenCount }
|
||||
: {}),
|
||||
...(typeof payload.mime_type === 'string'
|
||||
? { mimeType: payload.mime_type }
|
||||
: {}),
|
||||
...(typeof payload.extraction_status === 'string'
|
||||
? { extractionStatus: payload.extraction_status }
|
||||
: {}),
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -1662,6 +1685,8 @@ export default function MessageInput({
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDropOn={handleDropOn}
|
||||
unreadableIds={unreadableAttachmentIds}
|
||||
modelName={selectedModel?.display_name}
|
||||
/>
|
||||
|
||||
{sendArmed && sendReadiness.state === 'waiting' && (
|
||||
|
||||
@@ -13,6 +13,10 @@ type AttachmentChipListProps = {
|
||||
onDragStart: (e: React.DragEvent, id: string) => void;
|
||||
onDragOver: (e: React.DragEvent) => void;
|
||||
onDropOn: (e: React.DragEvent, targetId: string) => void;
|
||||
/** Completed attachments the selected model has no way to read. */
|
||||
unreadableIds?: Set<string>;
|
||||
/** Display name of the selected model, for the unreadable-file warning. */
|
||||
modelName?: string;
|
||||
};
|
||||
|
||||
export default function AttachmentChipList({
|
||||
@@ -22,6 +26,8 @@ export default function AttachmentChipList({
|
||||
onDragStart,
|
||||
onDragOver,
|
||||
onDropOn,
|
||||
unreadableIds,
|
||||
modelName,
|
||||
}: AttachmentChipListProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -31,6 +37,11 @@ export default function AttachmentChipList({
|
||||
const failures = attachments.filter(
|
||||
(attachment) => attachment.status === 'failed' && attachment.errorMessage,
|
||||
);
|
||||
// Not a failure: the file is kept and still sends. It only warns that the
|
||||
// model picked right now would receive nothing it can read.
|
||||
const unreadable = modelName
|
||||
? attachments.filter((attachment) => unreadableIds?.has(attachment.id))
|
||||
: [];
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -141,6 +152,22 @@ export default function AttachmentChipList({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{unreadable.length > 0 && (
|
||||
<div
|
||||
className="flex flex-col gap-0.5 px-2 pb-1 text-xs text-[#B54708] sm:px-3 dark:text-[#FDB022]"
|
||||
role="status"
|
||||
>
|
||||
{unreadable.map((attachment) => (
|
||||
<span key={attachment.id}>
|
||||
{t('conversation.attachments.unreadableByModel', {
|
||||
name: attachment.fileName,
|
||||
model: modelName,
|
||||
})}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { Attachment } from '../../upload/uploadSlice';
|
||||
import { cannotReadAttachment } from './attachmentReadability';
|
||||
|
||||
const att = (over: Partial<Attachment> = {}): Attachment => ({
|
||||
id: 'a1',
|
||||
fileName: 'scan.pdf',
|
||||
progress: 100,
|
||||
status: 'completed',
|
||||
taskId: 't1',
|
||||
...over,
|
||||
});
|
||||
|
||||
const PDF_AND_IMAGES = [
|
||||
'application/pdf',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/webp',
|
||||
];
|
||||
const IMAGES_ONLY = ['image/png', 'image/jpeg', 'image/webp'];
|
||||
const TEXT_ONLY: string[] = [];
|
||||
|
||||
const scannedPdf = att({
|
||||
mimeType: 'application/pdf',
|
||||
extractionStatus: 'no_text',
|
||||
token_count: 0,
|
||||
});
|
||||
const photo = att({
|
||||
fileName: 'photo.webp',
|
||||
mimeType: 'image/webp',
|
||||
extractionStatus: 'ok',
|
||||
token_count: 0,
|
||||
});
|
||||
|
||||
describe('cannotReadAttachment', () => {
|
||||
it('flags a scanned PDF on a text-only model', () => {
|
||||
expect(cannotReadAttachment(scannedPdf, TEXT_ONLY)).toBe(true);
|
||||
});
|
||||
|
||||
it('lets a model that reads PDFs natively take a scanned PDF', () => {
|
||||
expect(cannotReadAttachment(scannedPdf, PDF_AND_IMAGES)).toBe(false);
|
||||
});
|
||||
|
||||
it('lets an image-only model take a scanned PDF as page images', () => {
|
||||
expect(cannotReadAttachment(scannedPdf, IMAGES_ONLY)).toBe(false);
|
||||
});
|
||||
|
||||
it('flags an image on a text-only model', () => {
|
||||
expect(cannotReadAttachment(photo, TEXT_ONLY)).toBe(true);
|
||||
});
|
||||
|
||||
it('lets a vision model take the image', () => {
|
||||
expect(cannotReadAttachment(photo, IMAGES_ONLY)).toBe(false);
|
||||
});
|
||||
|
||||
it('does not flag a PDF whose text was extracted', () => {
|
||||
const textPdf = att({
|
||||
mimeType: 'application/pdf',
|
||||
extractionStatus: 'ok',
|
||||
token_count: 1200,
|
||||
});
|
||||
expect(cannotReadAttachment(textPdf, TEXT_ONLY)).toBe(false);
|
||||
});
|
||||
|
||||
it('stays quiet when the model or the file type is unknown', () => {
|
||||
expect(cannotReadAttachment(scannedPdf, undefined)).toBe(false);
|
||||
expect(
|
||||
cannotReadAttachment(att({ extractionStatus: 'no_text' }), TEXT_ONLY),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('ignores attachments that have not finished processing', () => {
|
||||
expect(
|
||||
cannotReadAttachment({ ...scannedPdf, status: 'processing' }, TEXT_ONLY),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { Attachment } from '../../upload/uploadSlice';
|
||||
|
||||
/**
|
||||
* Whether the selected model has no way to read a completed attachment.
|
||||
*
|
||||
* A file with extracted text reaches any model as text. A file without any
|
||||
* (an image, or a scanned PDF stored with `extraction_status: 'no_text'`)
|
||||
* only reaches a model that takes that format natively; a PDF also reaches a
|
||||
* model that takes images, which is sent its pages as images. When the model
|
||||
* or the file type is unknown, stay quiet rather than warn on a guess.
|
||||
*/
|
||||
export function cannotReadAttachment(
|
||||
attachment: Attachment,
|
||||
supportedTypes: string[] | undefined,
|
||||
): boolean {
|
||||
if (attachment.status !== 'completed') return false;
|
||||
const mime = attachment.mimeType;
|
||||
if (!mime || !supportedTypes) return false;
|
||||
|
||||
const isPdf = mime === 'application/pdf';
|
||||
if (!isPdf && !mime.startsWith('image/')) return false;
|
||||
|
||||
const hasText =
|
||||
attachment.extractionStatus !== 'no_text' &&
|
||||
(attachment.token_count ?? 0) > 0;
|
||||
if (hasText) return false;
|
||||
|
||||
if (supportedTypes.includes(mime)) return false;
|
||||
if (isPdf && supportedTypes.some((type) => type.startsWith('image/'))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1105,7 +1105,8 @@
|
||||
"waitingToSend_other": "Will send when {{count}} files finish processing…",
|
||||
"cancelQueuedSend": "Cancel",
|
||||
"sendBlockedByFailed": "{{names}} couldn't be processed — remove to send",
|
||||
"unsupportedType": "Not a supported file type ({{extension}})"
|
||||
"unsupportedType": "Not a supported file type ({{extension}})",
|
||||
"unreadableByModel": "{{model}} can't read {{name}} — switch to a model that supports images or PDFs"
|
||||
},
|
||||
"retry": "Retry",
|
||||
"reasoning": "Reasoning",
|
||||
|
||||
@@ -401,6 +401,24 @@ describe('attachment race recovery', () => {
|
||||
...overrides,
|
||||
});
|
||||
|
||||
it('keeps the mime type and extraction status a completed attachment reports', () => {
|
||||
let state = reducer(undefined, addAttachment(makeAttachment()));
|
||||
state = reducer(
|
||||
state,
|
||||
sseEventReceived(
|
||||
attEvent('attachment.completed', {
|
||||
token_count: 0,
|
||||
mime_type: 'application/pdf',
|
||||
extraction_status: 'no_text',
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
expect(state.attachments[0].status).toBe('completed');
|
||||
expect(state.attachments[0].mimeType).toBe('application/pdf');
|
||||
expect(state.attachments[0].extractionStatus).toBe('no_text');
|
||||
});
|
||||
|
||||
it('drops attachment.completed silently when no row matches attachmentId', () => {
|
||||
const state = reducer(
|
||||
undefined,
|
||||
|
||||
@@ -46,6 +46,13 @@ export interface Attachment {
|
||||
*/
|
||||
attachmentId?: string;
|
||||
token_count?: number;
|
||||
/** Mime type the server stored (a converted TIFF/BMP reports ``image/png``). */
|
||||
mimeType?: string;
|
||||
/**
|
||||
* ``metadata.extraction.status`` of a completed attachment: ``ok``, or
|
||||
* ``no_text`` for a scanned PDF kept for models that read it natively.
|
||||
*/
|
||||
extractionStatus?: string;
|
||||
/** Why a ``failed`` attachment failed, when known (server or client gate). */
|
||||
errorMessage?: string;
|
||||
}
|
||||
@@ -299,6 +306,12 @@ export const uploadSlice = createSlice({
|
||||
if (Number.isFinite(tokenCount)) {
|
||||
attachment.token_count = tokenCount;
|
||||
}
|
||||
if (typeof payload.mime_type === 'string') {
|
||||
attachment.mimeType = payload.mime_type;
|
||||
}
|
||||
if (typeof payload.extraction_status === 'string') {
|
||||
attachment.extractionStatus = payload.extraction_status;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'attachment.failed': {
|
||||
|
||||
Reference in new issue
Block a user