mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-14 00:16:20 +00:00
rabbit fixes
This commit is contained in:
1 parent
cdbc4695ea
commit
f4331cd3a3
31 files changed
+470
-87
No files matched your search
@@ -6,8 +6,11 @@
|
||||
// JSX tree as the `<Card variant="filled">`, not ones a child component
|
||||
// renders.
|
||||
|
||||
const FILLED_CARD =
|
||||
'JSXElement:has(> JSXOpeningElement[name.name="Card"]:has(> JSXAttribute[name.name="variant"][value.value="filled"]))';
|
||||
// `variant="filled"` or `variant={'filled'}`.
|
||||
const FILLED_VARIANT =
|
||||
'JSXAttribute[name.name="variant"]:matches([value.value="filled"], [value.expression.value="filled"])';
|
||||
|
||||
const FILLED_CARD = `JSXElement:has(> JSXOpeningElement[name.name="Card"]:has(> ${FILLED_VARIANT}))`;
|
||||
|
||||
// `bg-muted` or `bg-muted/NN`, with any variant prefix, but not
|
||||
// `bg-muted-foreground`.
|
||||
@@ -16,7 +19,7 @@ const BG_MUTED = '/(^|[\\s:])bg-muted([^-a-z]|$)/';
|
||||
/** @type {{ selector: string, message: string }[]} */
|
||||
export const cardSurfaceSelectors = [
|
||||
{
|
||||
selector: `${FILLED_CARD} JSXElement > JSXOpeningElement[name.name="Card"] > JSXAttribute[name.name="variant"][value.value="filled"]`,
|
||||
selector: `${FILLED_CARD} JSXElement > JSXOpeningElement[name.name="Card"] > ${FILLED_VARIANT}`,
|
||||
message:
|
||||
'A filled Card inside a filled Card is a fill on a fill. A well inside a tile needs a panel around it, or none. See DESIGN.md "Card surfaces".',
|
||||
},
|
||||
|
||||
@@ -31,7 +31,7 @@ export const everywhereSelectors = [
|
||||
'z-index comes from the stacking layers: z-10 sticky headers, z-20 in-page floating chrome, z-50 overlays, z-200 portalled floating lists. See DESIGN.md "Stacking".',
|
||||
),
|
||||
...inStrings(
|
||||
'/(^|\\s)(focus|focus-visible|focus-within):ring-2(\\s|$)/',
|
||||
'/(^|[\\s:-])(focus|focus-visible|focus-within):ring-2(\\s|$)/',
|
||||
'The focus ring is ring-3 ring-ring/50 on focus-visible (fields: focus-within on a frame the same way). ring-2 is only for selection rings. See DESIGN.md "Focus ring".',
|
||||
),
|
||||
{
|
||||
@@ -49,7 +49,7 @@ export const pageSelectors = [
|
||||
'Transition only the property that changes: transition-colors by default, transition-transform for chevrons, transition-shadow for a ring. See DESIGN.md "Motion".',
|
||||
),
|
||||
...inStrings(
|
||||
'/(^|\\s)hover:scale-(?!x-|y-)/',
|
||||
'/(^|[\\s:-])hover:scale-(?!x-|y-)/',
|
||||
'Hover is a fill, border or text-colour change, never a scale. See DESIGN.md "Motion".',
|
||||
),
|
||||
...inStrings(
|
||||
@@ -82,8 +82,8 @@ export const pageSelectors = [
|
||||
'Every table is ui/table (Table, TableHead, TableRow…), never a raw <table>. See DESIGN.md "Table".',
|
||||
},
|
||||
...[
|
||||
'JSXOpeningElement[name.name="a"] > JSXAttribute[name.name="className"] Literal[value=/(^|\\s)(underline|text-primary)(\\s|$)/]',
|
||||
'JSXOpeningElement[name.name="a"] > JSXAttribute[name.name="className"] TemplateElement[value.raw=/(^|\\s)(underline|text-primary)(\\s|$)/]',
|
||||
'JSXOpeningElement[name.name="a"] > JSXAttribute[name.name="className"] Literal[value=/(^|[\\s:])(underline|text-primary)(\\s|$)/]',
|
||||
'JSXOpeningElement[name.name="a"] > JSXAttribute[name.name="className"] TemplateElement[value.raw=/(^|[\\s:])(underline|text-primary)(\\s|$)/]',
|
||||
].map((selector) => ({
|
||||
selector,
|
||||
message:
|
||||
|
||||
@@ -789,18 +789,24 @@ describe('Chunks', () => {
|
||||
expect(container.querySelector('h1')?.textContent).toBe('Second');
|
||||
});
|
||||
|
||||
it('an edited chunk the store moved elsewhere is found by its id', async () => {
|
||||
it('an edited chunk the store moved off the probed positions keeps what is shown', async () => {
|
||||
serveStore(1);
|
||||
await render({ embedded: true });
|
||||
const tiles = container.querySelectorAll<HTMLButtonElement>(
|
||||
'button[data-slot="card"]',
|
||||
);
|
||||
await act(async () => tiles[2].click());
|
||||
await editOpenChunk('# Third edited');
|
||||
expect(container.querySelector('h1')?.textContent).toBe('Third edited');
|
||||
expect(position()).toEqual([2, 3]);
|
||||
await act(async () => buttonByLabel('settings.sources.nextChunk')!.click());
|
||||
expect(container.querySelector('h1')?.textContent).toBe('Second');
|
||||
await act(async () => tiles[0].click());
|
||||
service.getDocumentChunks.mockClear();
|
||||
await editOpenChunk('# First edited');
|
||||
expect(container.querySelector('h1')?.textContent).toBe('First edited');
|
||||
expect(position()).toEqual([1, 3]);
|
||||
// Only the open and last positions are probed: the whole filtered list
|
||||
// (one page of `total`) is never fetched.
|
||||
const sizes = service.getDocumentChunks.mock.calls.map(
|
||||
(call: unknown[]) => call[2],
|
||||
);
|
||||
expect(sizes).toContain(1);
|
||||
expect(sizes).not.toContain(3);
|
||||
});
|
||||
|
||||
it('an edited chunk that keeps its place stays put', async () => {
|
||||
|
||||
@@ -211,10 +211,10 @@ const Chunks: React.FC<ChunksProps> = ({
|
||||
* After a save, find the edited chunk in the store's current order and show
|
||||
* its stored copy (fresh metadata) at that position, so "n of total" and
|
||||
* previous / next follow the list as it now is. Checked in turn: the open
|
||||
* position (the chunk stayed), the last one (a store that appends the new
|
||||
* copy), then the whole filtered list. Each probe matches by id, so no
|
||||
* store ordering is assumed. When the chunk is not in the list (a search
|
||||
* it no longer matches), what is shown stays.
|
||||
* position (the chunk stayed), then the last one (a store that re-adds the
|
||||
* copy at the end). Each probe matches by id, so no store ordering is
|
||||
* assumed. When neither holds it (moved elsewhere, or a search it no longer
|
||||
* matches), what is shown stays: the whole filtered list is never fetched.
|
||||
*
|
||||
* @param chunk The chunk just saved, with its new id.
|
||||
*/
|
||||
@@ -249,25 +249,11 @@ const Chunks: React.FC<ChunksProps> = ({
|
||||
return;
|
||||
}
|
||||
if (total < 1) return;
|
||||
if (total !== openPosition) {
|
||||
const last = await fetchPage(total, 1);
|
||||
if (!last) return;
|
||||
const lastChunk: ChunkType | undefined = last.chunks?.[0];
|
||||
if (lastChunk?.doc_id === chunk.doc_id) {
|
||||
show(lastChunk, total, total);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const all = await fetchPage(1, total);
|
||||
if (!all) return;
|
||||
const list: ChunkType[] = all.chunks ?? [];
|
||||
const index = list.findIndex((c) => c.doc_id === chunk.doc_id);
|
||||
if (index >= 0)
|
||||
show(
|
||||
list[index],
|
||||
index + 1,
|
||||
typeof all.total === 'number' ? all.total : total,
|
||||
);
|
||||
if (total === openPosition) return;
|
||||
const last = await fetchPage(total, 1);
|
||||
if (!last) return;
|
||||
const lastChunk: ChunkType | undefined = last.chunks?.[0];
|
||||
if (lastChunk?.doc_id === chunk.doc_id) show(lastChunk, total, total);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
@@ -394,10 +380,11 @@ const Chunks: React.FC<ChunksProps> = ({
|
||||
if (!response.ok) throw new Error('Failed to update chunk');
|
||||
const data = await response.json().catch(() => ({}));
|
||||
closeSheet();
|
||||
// The edit is saved under a new id. Show it straight away (its token
|
||||
// count is recomputed server-side, so it is dropped until the reload),
|
||||
// then find where the store now keeps it: the update re-adds the chunk,
|
||||
// so it can move (FAISS appends it; pgvector has no ORDER BY).
|
||||
// Show the edit straight away (its token count is recomputed
|
||||
// server-side, so it is dropped until the reload), then find where the
|
||||
// store now keeps it. Most stores (FAISS, MongoDB, pgvector, Qdrant)
|
||||
// update in place: same id, same position. Only the base add-then-delete
|
||||
// fallback (Milvus) saves it under a new id, which can move it.
|
||||
const edited: ChunkType = {
|
||||
...chunk,
|
||||
doc_id: data.chunk_id ?? chunk.doc_id,
|
||||
|
||||
@@ -27,6 +27,11 @@ function lint(code: string): string[] {
|
||||
|
||||
describe('card surface lint: no fill on a fill', () => {
|
||||
it('rejects bg-muted on an element inside a filled tile, in cn() too', () => {
|
||||
expect(
|
||||
lint(
|
||||
`<Card variant={'filled'}><div className="bg-muted px-4">x</div></Card>`,
|
||||
),
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
lint(
|
||||
`<Card variant="filled"><div className="bg-muted px-4">x</div></Card>`,
|
||||
@@ -45,12 +50,25 @@ describe('card surface lint: no fill on a fill', () => {
|
||||
`<Card variant="filled"><Card variant="filled" padding="sm">x</Card></Card>`,
|
||||
),
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
lint(
|
||||
`<Card variant={'filled'}><Card variant="filled" padding="sm">x</Card></Card>`,
|
||||
),
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
lint(
|
||||
`<Card variant="filled"><Card variant={'filled'} padding="sm">x</Card></Card>`,
|
||||
),
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('rejects a Skeleton without surface="muted" on a filled tile', () => {
|
||||
expect(
|
||||
lint(`<Card variant="filled"><Skeleton className="h-4" /></Card>`),
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
lint(`<Card variant={'filled'}><Skeleton className="h-4" /></Card>`),
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
lint(
|
||||
`<Card variant="filled"><Skeleton surface="muted" className="h-4" /></Card>`,
|
||||
|
||||
@@ -44,14 +44,24 @@ describe('design rules lint', () => {
|
||||
['off-scale z', `<div className="absolute z-30" />`],
|
||||
['arbitrary z', `<div className="z-[60]" />`],
|
||||
['focus ring-2', `<div className="focus-within:ring-2" />`],
|
||||
['md: focus ring-2', `<div className="md:focus-visible:ring-2" />`],
|
||||
['dark: focus ring-2', `<div className="dark:focus-visible:ring-2" />`],
|
||||
['peer focus ring-2', `<div className="peer-focus-visible:ring-2" />`],
|
||||
['lucide loader', `import { Loader2 } from 'lucide-react';`],
|
||||
['transition-all', `<div className="transition-all duration-200" />`],
|
||||
['hover scale', `<div className="hover:scale-105" />`],
|
||||
['md:hover scale', `<div className="md:hover:scale-105" />`],
|
||||
['group-hover scale', `<div className="group-hover:scale-105" />`],
|
||||
['bg-primary/10', `const tone = 'bg-primary/10 text-primary';`],
|
||||
['native checkbox', `<input type="checkbox" />`],
|
||||
['title on Button', `<Button title="Delete" />`],
|
||||
['raw table', `<table><tbody /></table>`],
|
||||
['link-styled a', `<a className="text-primary underline" href="#">x</a>`],
|
||||
['hover: underline a', `<a className="hover:underline" href="#">x</a>`],
|
||||
[
|
||||
'dark: text-primary a',
|
||||
`<a className={\`dark:text-primary \${x}\`} href="#">x</a>`,
|
||||
],
|
||||
['drawer width', `<SheetContent side="right" className="sm:max-w-xl" />`],
|
||||
['drawer w-', `<SheetContent className="w-80 p-0" />`],
|
||||
])('rejects %s', (_name, code) => {
|
||||
@@ -70,6 +80,9 @@ describe('design rules lint', () => {
|
||||
['size and max-w', `<div className="size-4 max-w-[520px] min-h-dvh" />`],
|
||||
['selection ring-2', `<div className="ring-2 ring-primary" />`],
|
||||
['hover:scale-x underline wipe', `<span className="hover:scale-x-100" />`],
|
||||
['md:hover:scale-x wipe', `<span className="md:hover:scale-x-100" />`],
|
||||
['no-underline a', `<a className="no-underline" href="#">x</a>`],
|
||||
['underline-offset a', `<a className="underline-offset-2" href="#">x</a>`],
|
||||
['transition-colors', `<div className="transition-colors" />`],
|
||||
[
|
||||
'other lucide icons',
|
||||
|
||||
@@ -1101,7 +1101,6 @@
|
||||
"dropzoneText": "Zum Hochladen klicken oder per Drag & Drop ablegen",
|
||||
"dropzoneHint": "PDF, Word, Excel, PowerPoint, Markdown, HTML, Bilder, Audio, EPUB, JSON oder ZIP, jeweils bis zu 25 MB",
|
||||
"filesRejected": "Nicht hinzugefügt (größer als 25 MB oder kein unterstützter Dateityp): {{files}}",
|
||||
"info": "Bitte lade .pdf, .txt, .rst, .csv, .xlsx, .xlsm, .xlsb, .xls, .ods, .docx, .docm, .doc, .odt, .rtf, .md, .html, .xhtml, .png, .jpg, .jpeg, .epub, .json, .pptx, .pptm, .ppt, .pps, .ppsx, .ppsm, .pot, .odp, .zip hoch (max. 25 MB)",
|
||||
"uploadedFiles": "Hochgeladene Dateien",
|
||||
"cancel": "Abbrechen",
|
||||
"train": "Trainieren",
|
||||
|
||||
@@ -1107,7 +1107,6 @@
|
||||
"dropzoneText": "Click to upload or drag and drop",
|
||||
"dropzoneHint": "PDF, Word, Excel, PowerPoint, Markdown, HTML, images, audio, EPUB, JSON or ZIP, up to 25 MB each",
|
||||
"filesRejected": "Not added (over 25 MB or not a supported type): {{files}}",
|
||||
"info": "Please upload .pdf, .txt, .rst, .csv, .xlsx, .xlsm, .xlsb, .xls, .ods, .docx, .docm, .doc, .odt, .rtf, .md, .html, .xhtml, .png, .jpg, .jpeg, .epub, .json, .pptx, .pptm, .ppt, .pps, .ppsx, .ppsm, .pot, .odp, .zip limited to 25mb",
|
||||
"uploadedFiles": "Uploaded Files",
|
||||
"cancel": "Cancel",
|
||||
"train": "Train",
|
||||
|
||||
@@ -1101,7 +1101,6 @@
|
||||
"dropzoneText": "Haz clic para subir o arrastra y suelta",
|
||||
"dropzoneHint": "PDF, Word, Excel, PowerPoint, Markdown, HTML, imágenes, audio, EPUB, JSON o ZIP, hasta 25 MB cada uno",
|
||||
"filesRejected": "No se añadieron (más de 25 MB o tipo no admitido): {{files}}",
|
||||
"info": "Por favor, sube archivos .pdf, .txt, .rst, .csv, .xlsx, .xlsm, .xlsb, .xls, .ods, .docx, .docm, .doc, .odt, .rtf, .md, .html, .xhtml, .png, .jpg, .jpeg, .epub, .json, .pptx, .pptm, .ppt, .pps, .ppsx, .ppsm, .pot, .odp, .zip limitados a 25MB",
|
||||
"uploadedFiles": "Archivos Subidos",
|
||||
"cancel": "Cancelar",
|
||||
"train": "Entrenar",
|
||||
|
||||
@@ -1092,7 +1092,6 @@
|
||||
"dropzoneText": "クリックしてアップロード、またはドラッグ&ドロップ",
|
||||
"dropzoneHint": "PDF、Word、Excel、PowerPoint、Markdown、HTML、画像、音声、EPUB、JSON、ZIP(各 25 MB まで)",
|
||||
"filesRejected": "追加されませんでした(25 MB を超えているか、未対応の形式です): {{files}}",
|
||||
"info": "25MBまでの.pdf、.txt、.rst、.csv、.xlsx、.xlsm、.xlsb、.xls、.ods、.docx、.docm、.doc、.odt、.rtf、.md、.html、.xhtml、.png、.jpg、.jpeg、.epub、.json、.pptx、.pptm、.ppt、.pps、.ppsx、.ppsm、.pot、.odp、.zipファイルをアップロードしてください",
|
||||
"uploadedFiles": "アップロードされたファイル",
|
||||
"cancel": "キャンセル",
|
||||
"train": "トレーニング",
|
||||
|
||||
@@ -1157,7 +1157,6 @@
|
||||
"dropzoneText": "Нажмите, чтобы загрузить, или перетащите файлы",
|
||||
"dropzoneHint": "PDF, Word, Excel, PowerPoint, Markdown, HTML, изображения, аудио, EPUB, JSON или ZIP, до 25 МБ каждый",
|
||||
"filesRejected": "Не добавлены (больше 25 МБ или неподдерживаемый формат): {{files}}",
|
||||
"info": "Пожалуйста, загрузите файлы .pdf, .txt, .rst, .csv, .xlsx, .xlsm, .xlsb, .xls, .ods, .docx, .docm, .doc, .odt, .rtf, .md, .html, .xhtml, .png, .jpg, .jpeg, .epub, .json, .pptx, .pptm, .ppt, .pps, .ppsx, .ppsm, .pot, .odp, .zip размером до 25 МБ",
|
||||
"uploadedFiles": "Загруженные файлы",
|
||||
"cancel": "Отмена",
|
||||
"train": "Тренировка",
|
||||
|
||||
@@ -1092,7 +1092,6 @@
|
||||
"dropzoneText": "點擊上傳或拖放檔案",
|
||||
"dropzoneHint": "PDF、Word、Excel、PowerPoint、Markdown、HTML、圖片、音訊、EPUB、JSON 或 ZIP,每個最大 25 MB",
|
||||
"filesRejected": "未新增(超過 25 MB 或不支援的類型):{{files}}",
|
||||
"info": "請上傳限制為25MB的.pdf、.txt、.rst、.csv、.xlsx、.xlsm、.xlsb、.xls、.ods、.docx、.docm、.doc、.odt、.rtf、.md、.html、.xhtml、.png、.jpg、.jpeg、.epub、.json、.pptx、.pptm、.ppt、.pps、.ppsx、.ppsm、.pot、.odp、.zip檔案",
|
||||
"uploadedFiles": "已上傳檔案",
|
||||
"cancel": "取消",
|
||||
"train": "訓練",
|
||||
|
||||
@@ -1092,7 +1092,6 @@
|
||||
"dropzoneText": "点击上传或拖放文件",
|
||||
"dropzoneHint": "PDF、Word、Excel、PowerPoint、Markdown、HTML、图片、音频、EPUB、JSON 或 ZIP,每个最大 25 MB",
|
||||
"filesRejected": "未添加(超过 25 MB 或不支持的类型):{{files}}",
|
||||
"info": "请上传限制为25MB的.pdf、.txt、.rst、.csv、.xlsx、.xlsm、.xlsb、.xls、.ods、.docx、.docm、.doc、.odt、.rtf、.md、.html、.xhtml、.png、.jpg、.jpeg、.epub、.json、.pptx、.pptm、.ppt、.pps、.ppsx、.ppsm、.pot、.odp、.zip文件",
|
||||
"uploadedFiles": "已上传文件",
|
||||
"cancel": "取消",
|
||||
"train": "训练",
|
||||
|
||||
@@ -93,6 +93,7 @@ describe('Prompts', () => {
|
||||
const row = container.querySelector('[data-slot="setting-row"]')!;
|
||||
const label = row.querySelector<HTMLLabelElement>('label')!;
|
||||
expect(label.textContent).toBe('settings.general.prompt');
|
||||
expect(trigger.id).toBeTruthy();
|
||||
expect(label.htmlFor).toBe(trigger.id);
|
||||
expect(row.textContent).toContain('Used without an agent.');
|
||||
expect(trigger.hasAttribute('aria-label')).toBe(false);
|
||||
@@ -140,6 +141,7 @@ describe('Prompts', () => {
|
||||
const trigger = container.querySelector<HTMLButtonElement>(
|
||||
'button[role="combobox"]',
|
||||
)!;
|
||||
expect(trigger.id).toBeTruthy();
|
||||
expect(label.htmlFor).toBe(trigger.id);
|
||||
expect(trigger.hasAttribute('aria-label')).toBe(false);
|
||||
expect(trigger.className).toContain('w-full');
|
||||
|
||||
Reference in new issue
Block a user