mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-01-09 07:32:05 +08:00
💄 style: Unzip file when uploading in knowledge base [LOB-500] (#9854)
* feat: Unzip file * feat: Limit max file upload limit * fix: Remove unused test * opti: Update translation * style: Adjust padding * feat: Update translation * fix: Test error * fix: Test erro * fix: Test * fix: test error * fix: Test * feat: Rremove message
This commit is contained in:
parent
1fe6a5997b
commit
e568ce6f31
@ -163,9 +163,9 @@
|
||||
"addMember": "إضافة عضو",
|
||||
"allMembers": "جميع الأعضاء",
|
||||
"createGroup": "إنشاء فريق وكيل",
|
||||
"noAvailableAgents": "لا يوجد مساعدين متاحين للدعوة",
|
||||
"noSelectedAgents": "لم يتم اختيار مساعدين بعد",
|
||||
"searchAgents": "البحث عن مساعدين...",
|
||||
"noAvailableAgents": "لا يوجد وكلاء متاحون للدعوة",
|
||||
"noSelectedAgents": "لم يتم اختيار أي وكيل بعد",
|
||||
"searchAgents": "ابحث عن وكيل...",
|
||||
"setInitialMembers": "اختيار أعضاء الفريق"
|
||||
},
|
||||
"members": "الأعضاء",
|
||||
@ -229,7 +229,7 @@
|
||||
"jumpToMessage": "الانتقال إلى الرسالة رقم {{index}}",
|
||||
"nextMessage": "الرسالة التالية",
|
||||
"previousMessage": "الرسالة السابقة",
|
||||
"senderAssistant": "المساعد",
|
||||
"senderAssistant": "الوكيل",
|
||||
"senderUser": "أنت"
|
||||
},
|
||||
"newAgent": "مساعد جديد",
|
||||
|
||||
@ -85,6 +85,7 @@
|
||||
"restTime": "الوقت المتبقي {{time}}"
|
||||
}
|
||||
},
|
||||
"fileQueueInfo": "يتم حاليًا تحميل {{count}} ملفًا، وسيتم وضع {{remaining}} ملفًا في قائمة الانتظار للتحميل",
|
||||
"totalCount": "إجمالي {{count}} عنصر",
|
||||
"uploadStatus": {
|
||||
"error": "حدث خطأ أثناء الرفع",
|
||||
|
||||
@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V مبني على نموذج GLM-4.5-Air الأساسي، يرث التقنيات المثبتة من GLM-4.1V-Thinking، ويوسعها بفعالية من خلال بنية MoE القوية التي تضم 106 مليار معلمة."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -163,9 +163,9 @@
|
||||
"addMember": "Добавяне на член",
|
||||
"allMembers": "Всички членове",
|
||||
"createGroup": "Създаване на екип на Agent",
|
||||
"noAvailableAgents": "Няма налични асистенти за покана",
|
||||
"noSelectedAgents": "Все още не са избрани асистенти",
|
||||
"searchAgents": "Търсене на асистенти...",
|
||||
"noAvailableAgents": "Няма налични агенти за покана",
|
||||
"noSelectedAgents": "Все още не са избрани агенти",
|
||||
"searchAgents": "Търсене на агент...",
|
||||
"setInitialMembers": "Избор на членове на екипа"
|
||||
},
|
||||
"members": "Членове",
|
||||
@ -229,7 +229,7 @@
|
||||
"jumpToMessage": "Отиди до съобщение № {{index}}",
|
||||
"nextMessage": "Следващо съобщение",
|
||||
"previousMessage": "Предишно съобщение",
|
||||
"senderAssistant": "Асистент",
|
||||
"senderAssistant": "Агент",
|
||||
"senderUser": "Ти"
|
||||
},
|
||||
"newAgent": "Нов агент",
|
||||
|
||||
@ -85,6 +85,7 @@
|
||||
"restTime": "Остава {{time}}"
|
||||
}
|
||||
},
|
||||
"fileQueueInfo": "Качват се първите {{count}} файла, останалите {{remaining}} ще бъдат поставени в опашка за качване",
|
||||
"totalCount": "Общо {{count}} елемента",
|
||||
"uploadStatus": {
|
||||
"error": "Грешка при качване",
|
||||
|
||||
@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V е изграден върху основния модел GLM-4.5-Air, наследявайки проверените технологии на GLM-4.1V-Thinking и постига ефективно мащабиране чрез мощната MoE архитектура с 106 милиарда параметри."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -163,9 +163,9 @@
|
||||
"addMember": "Mitglied hinzufügen",
|
||||
"allMembers": "Alle Mitglieder",
|
||||
"createGroup": "Agenten-Team erstellen",
|
||||
"noAvailableAgents": "Keine verfügbaren Assistenten zum Einladen",
|
||||
"noSelectedAgents": "Noch keine Assistenten ausgewählt",
|
||||
"searchAgents": "Assistenten suchen...",
|
||||
"noAvailableAgents": "Keine verfügbaren Agents zum Einladen",
|
||||
"noSelectedAgents": "Noch keine Agents ausgewählt",
|
||||
"searchAgents": "Agents suchen...",
|
||||
"setInitialMembers": "Teammitglieder auswählen"
|
||||
},
|
||||
"members": "Mitglieder",
|
||||
@ -229,7 +229,7 @@
|
||||
"jumpToMessage": "Zur Nachricht Nr. {{index}} springen",
|
||||
"nextMessage": "Nächste Nachricht",
|
||||
"previousMessage": "Vorherige Nachricht",
|
||||
"senderAssistant": "Assistent",
|
||||
"senderAssistant": "Agent",
|
||||
"senderUser": "Du"
|
||||
},
|
||||
"newAgent": "Neuer Assistent",
|
||||
|
||||
@ -85,6 +85,7 @@
|
||||
"restTime": "Verbleibende Zeit {{time}}"
|
||||
}
|
||||
},
|
||||
"fileQueueInfo": "Die ersten {{count}} Dateien werden hochgeladen, die verbleibenden {{remaining}} Dateien werden in die Warteschlange gestellt",
|
||||
"totalCount": "Insgesamt {{count}} Elemente",
|
||||
"uploadStatus": {
|
||||
"error": "Fehler beim Hochladen",
|
||||
|
||||
@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V basiert auf dem GLM-4.5-Air Basismodell, übernimmt bewährte Techniken von GLM-4.1V-Thinking und skaliert effektiv mit einer leistungsstarken MoE-Architektur mit 106 Milliarden Parametern."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -163,9 +163,9 @@
|
||||
"addMember": "Add Member",
|
||||
"allMembers": "All members",
|
||||
"createGroup": "Create Agent Team",
|
||||
"noAvailableAgents": "No assistants available to invite",
|
||||
"noSelectedAgents": "No assistants selected yet",
|
||||
"searchAgents": "Search assistants...",
|
||||
"noAvailableAgents": "No available agents to invite",
|
||||
"noSelectedAgents": "No agents selected yet",
|
||||
"searchAgents": "Search agents...",
|
||||
"setInitialMembers": "Select Team Members"
|
||||
},
|
||||
"members": "Members",
|
||||
@ -229,7 +229,7 @@
|
||||
"jumpToMessage": "Jump to message {{index}}",
|
||||
"nextMessage": "Next message",
|
||||
"previousMessage": "Previous message",
|
||||
"senderAssistant": "Assistant",
|
||||
"senderAssistant": "Agent",
|
||||
"senderUser": "You"
|
||||
},
|
||||
"newAgent": "New Assistant",
|
||||
|
||||
@ -85,6 +85,7 @@
|
||||
"restTime": "Remaining {{time}}"
|
||||
}
|
||||
},
|
||||
"fileQueueInfo": "Uploading the first {{count}} files, {{remaining}} remaining in queue",
|
||||
"totalCount": "Total {{count}} items",
|
||||
"uploadStatus": {
|
||||
"error": "Upload error",
|
||||
|
||||
@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V is built on the GLM-4.5-Air foundational model, inheriting the proven techniques of GLM-4.1V-Thinking while achieving efficient scaling through a powerful 106 billion parameter MoE architecture."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -163,9 +163,9 @@
|
||||
"addMember": "Agregar miembro",
|
||||
"allMembers": "Todos los miembros",
|
||||
"createGroup": "Crear equipo de agentes",
|
||||
"noAvailableAgents": "No hay asistentes disponibles para invitar",
|
||||
"noSelectedAgents": "No se ha seleccionado ningún asistente",
|
||||
"searchAgents": "Buscar asistentes...",
|
||||
"noAvailableAgents": "No hay agentes disponibles para invitar",
|
||||
"noSelectedAgents": "Aún no se ha seleccionado ningún agente",
|
||||
"searchAgents": "Buscar agentes...",
|
||||
"setInitialMembers": "Seleccionar miembros del equipo"
|
||||
},
|
||||
"members": "Miembros",
|
||||
@ -229,7 +229,7 @@
|
||||
"jumpToMessage": "Ir al mensaje número {{index}}",
|
||||
"nextMessage": "Mensaje siguiente",
|
||||
"previousMessage": "Mensaje anterior",
|
||||
"senderAssistant": "Asistente",
|
||||
"senderAssistant": "Agente",
|
||||
"senderUser": "Tú"
|
||||
},
|
||||
"newAgent": "Nuevo asistente",
|
||||
|
||||
@ -85,6 +85,7 @@
|
||||
"restTime": "Tiempo restante {{time}}"
|
||||
}
|
||||
},
|
||||
"fileQueueInfo": "Subiendo los primeros {{count}} archivos, los {{remaining}} restantes se pondrán en cola para subir",
|
||||
"totalCount": "Total {{count}} elementos",
|
||||
"uploadStatus": {
|
||||
"error": "Error en la subida",
|
||||
|
||||
@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V está construido sobre el modelo base GLM-4.5-Air, heredando la tecnología verificada de GLM-4.1V-Thinking y logrando una escalabilidad eficiente mediante una potente arquitectura MoE de 106 mil millones de parámetros."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -163,9 +163,9 @@
|
||||
"addMember": "افزودن عضو",
|
||||
"allMembers": "تمام اعضا",
|
||||
"createGroup": "ایجاد تیم Agent",
|
||||
"noAvailableAgents": "دستیار قابل دعوت وجود ندارد",
|
||||
"noSelectedAgents": "هنوز دستیار انتخاب نشده است",
|
||||
"searchAgents": "جستجوی دستیار...",
|
||||
"noAvailableAgents": "هیچ عاملی برای دعوت در دسترس نیست",
|
||||
"noSelectedAgents": "هنوز عاملی انتخاب نشده است",
|
||||
"searchAgents": "جستجوی عامل...",
|
||||
"setInitialMembers": "انتخاب اعضای تیم"
|
||||
},
|
||||
"members": "اعضا",
|
||||
@ -229,7 +229,7 @@
|
||||
"jumpToMessage": "رفتن به پیام شماره {{index}}",
|
||||
"nextMessage": "پیام بعدی",
|
||||
"previousMessage": "پیام قبلی",
|
||||
"senderAssistant": "دستیار",
|
||||
"senderAssistant": "عامل",
|
||||
"senderUser": "شما"
|
||||
},
|
||||
"newAgent": "دستیار جدید",
|
||||
|
||||
@ -85,6 +85,7 @@
|
||||
"restTime": "زمان باقیمانده {{time}}"
|
||||
}
|
||||
},
|
||||
"fileQueueInfo": "در حال بارگذاری {{count}} فایل اول، {{remaining}} فایل باقیمانده در صف بارگذاری قرار خواهند گرفت",
|
||||
"totalCount": "مجموعاً {{count}} مورد",
|
||||
"uploadStatus": {
|
||||
"error": "خطا در بارگذاری",
|
||||
|
||||
@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V بر پایه مدل پایه GLM-4.5-Air ساخته شده است، فناوری اثبات شده GLM-4.1V-Thinking را به ارث برده و در عین حال با معماری قدرتمند MoE با 106 میلیارد پارامتر به طور مؤثر مقیاسپذیر شده است."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -163,9 +163,9 @@
|
||||
"addMember": "Ajouter un membre",
|
||||
"allMembers": "Tous les membres",
|
||||
"createGroup": "Créer un groupe d'agents",
|
||||
"noAvailableAgents": "Aucun assistant disponible à inviter",
|
||||
"noSelectedAgents": "Aucun assistant sélectionné",
|
||||
"searchAgents": "Rechercher un assistant...",
|
||||
"noAvailableAgents": "Aucun agent disponible à inviter",
|
||||
"noSelectedAgents": "Aucun agent sélectionné pour le moment",
|
||||
"searchAgents": "Rechercher un agent...",
|
||||
"setInitialMembers": "Sélectionner les membres du groupe"
|
||||
},
|
||||
"members": "Membres",
|
||||
@ -229,7 +229,7 @@
|
||||
"jumpToMessage": "Aller au message n° {{index}}",
|
||||
"nextMessage": "Message suivant",
|
||||
"previousMessage": "Message précédent",
|
||||
"senderAssistant": "Assistant",
|
||||
"senderAssistant": "Agent",
|
||||
"senderUser": "Vous"
|
||||
},
|
||||
"newAgent": "Nouvel agent",
|
||||
|
||||
@ -85,6 +85,7 @@
|
||||
"restTime": "Temps restant {{time}}"
|
||||
}
|
||||
},
|
||||
"fileQueueInfo": "Téléversement des {{count}} premiers fichiers en cours, les {{remaining}} fichiers restants seront mis en file d’attente",
|
||||
"totalCount": "Total {{count}} éléments",
|
||||
"uploadStatus": {
|
||||
"error": "Erreur de téléchargement",
|
||||
|
||||
@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V est construit sur le modèle de base GLM-4.5-Air, héritant des techniques éprouvées de GLM-4.1V-Thinking, tout en réalisant une mise à l'échelle efficace grâce à une puissante architecture MoE de 106 milliards de paramètres."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -163,9 +163,9 @@
|
||||
"addMember": "Aggiungi membro",
|
||||
"allMembers": "Tutti i membri",
|
||||
"createGroup": "Crea un team di Agent",
|
||||
"noAvailableAgents": "Nessun assistente disponibile da invitare",
|
||||
"noSelectedAgents": "Nessun assistente selezionato",
|
||||
"searchAgents": "Cerca assistenti...",
|
||||
"noAvailableAgents": "Nessun agente disponibile da invitare",
|
||||
"noSelectedAgents": "Nessun agente selezionato",
|
||||
"searchAgents": "Cerca agenti...",
|
||||
"setInitialMembers": "Seleziona i membri del team"
|
||||
},
|
||||
"members": "Membri",
|
||||
@ -229,7 +229,7 @@
|
||||
"jumpToMessage": "Vai al messaggio n. {{index}}",
|
||||
"nextMessage": "Messaggio successivo",
|
||||
"previousMessage": "Messaggio precedente",
|
||||
"senderAssistant": "Assistente",
|
||||
"senderAssistant": "Agente",
|
||||
"senderUser": "Tu"
|
||||
},
|
||||
"newAgent": "Nuovo assistente",
|
||||
|
||||
@ -85,6 +85,7 @@
|
||||
"restTime": "Tempo rimanente {{time}}"
|
||||
}
|
||||
},
|
||||
"fileQueueInfo": "Caricamento in corso dei primi {{count}} file, i restanti {{remaining}} file saranno messi in coda",
|
||||
"totalCount": "Totale {{count}} elementi",
|
||||
"uploadStatus": {
|
||||
"error": "Errore di caricamento",
|
||||
|
||||
@ -163,9 +163,9 @@
|
||||
"addMember": "メンバーを追加",
|
||||
"allMembers": "全メンバー",
|
||||
"createGroup": "エージェントチームを作成",
|
||||
"noAvailableAgents": "招待可能なアシスタントがいません",
|
||||
"noSelectedAgents": "アシスタントが選択されていません",
|
||||
"searchAgents": "アシスタントを検索...",
|
||||
"noAvailableAgents": "招待可能なエージェントがいません",
|
||||
"noSelectedAgents": "エージェントがまだ選択されていません",
|
||||
"searchAgents": "エージェントを検索...",
|
||||
"setInitialMembers": "チームメンバーを選択"
|
||||
},
|
||||
"members": "メンバー",
|
||||
@ -229,7 +229,7 @@
|
||||
"jumpToMessage": "メッセージ {{index}} へジャンプ",
|
||||
"nextMessage": "次のメッセージ",
|
||||
"previousMessage": "前のメッセージ",
|
||||
"senderAssistant": "アシスタント",
|
||||
"senderAssistant": "エージェント",
|
||||
"senderUser": "あなた"
|
||||
},
|
||||
"newAgent": "新しいエージェント",
|
||||
|
||||
@ -85,6 +85,7 @@
|
||||
"restTime": "残り {{time}}"
|
||||
}
|
||||
},
|
||||
"fileQueueInfo": "最初の {{count}} 件のファイルをアップロード中、残りの {{remaining}} 件は順番待ちです",
|
||||
"totalCount": "合計 {{count}} 件",
|
||||
"uploadStatus": {
|
||||
"error": "アップロードエラー",
|
||||
|
||||
@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V は GLM-4.5-Air 基盤モデルに基づき、GLM-4.1V-Thinking の検証済み技術を継承しつつ、強力な1060億パラメータの MoE アーキテクチャで効率的にスケールアップしています。"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -163,9 +163,9 @@
|
||||
"addMember": "멤버 추가",
|
||||
"allMembers": "전체 멤버",
|
||||
"createGroup": "Agent 팀 만들기",
|
||||
"noAvailableAgents": "초대할 보조자가 없습니다",
|
||||
"noSelectedAgents": "아직 보조자를 선택하지 않았습니다",
|
||||
"searchAgents": "보조자 검색...",
|
||||
"noAvailableAgents": "초대할 수 있는 에이전트가 없습니다",
|
||||
"noSelectedAgents": "선택된 에이전트가 없습니다",
|
||||
"searchAgents": "에이전트 검색...",
|
||||
"setInitialMembers": "팀 구성원 선택"
|
||||
},
|
||||
"members": "구성원",
|
||||
@ -229,7 +229,7 @@
|
||||
"jumpToMessage": "{{index}}번째 메시지로 이동",
|
||||
"nextMessage": "다음 메시지",
|
||||
"previousMessage": "이전 메시지",
|
||||
"senderAssistant": "도우미",
|
||||
"senderAssistant": "에이전트",
|
||||
"senderUser": "당신"
|
||||
},
|
||||
"newAgent": "새 도우미",
|
||||
|
||||
@ -85,6 +85,7 @@
|
||||
"restTime": "남은 시간 {{time}}"
|
||||
}
|
||||
},
|
||||
"fileQueueInfo": "{{count}}개 파일을 업로드 중이며, 나머지 {{remaining}}개 파일은 대기 중입니다",
|
||||
"totalCount": "총 {{count}}개 항목",
|
||||
"uploadStatus": {
|
||||
"error": "업로드 오류",
|
||||
|
||||
@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V는 GLM-4.5-Air 기본 모델을 기반으로 구축되었으며, 검증된 GLM-4.1V-Thinking 기술을 계승하면서 강력한 1060억 매개변수 MoE 아키텍처를 통해 효율적인 확장을 실현했습니다."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -163,9 +163,9 @@
|
||||
"addMember": "Lid toevoegen",
|
||||
"allMembers": "Alle leden",
|
||||
"createGroup": "Agent-team aanmaken",
|
||||
"noAvailableAgents": "Geen assistenten beschikbaar om uit te nodigen",
|
||||
"noSelectedAgents": "Nog geen assistenten geselecteerd",
|
||||
"searchAgents": "Assistenten zoeken...",
|
||||
"noAvailableAgents": "Geen beschikbare Agent om uit te nodigen",
|
||||
"noSelectedAgents": "Nog geen Agent geselecteerd",
|
||||
"searchAgents": "Zoek Agent...",
|
||||
"setInitialMembers": "Selecteer teamleden"
|
||||
},
|
||||
"members": "Leden",
|
||||
@ -229,7 +229,7 @@
|
||||
"jumpToMessage": "Ga naar bericht {{index}}",
|
||||
"nextMessage": "Volgend bericht",
|
||||
"previousMessage": "Vorig bericht",
|
||||
"senderAssistant": "Assistent",
|
||||
"senderAssistant": "Agent",
|
||||
"senderUser": "Jij"
|
||||
},
|
||||
"newAgent": "Nieuwe assistent",
|
||||
|
||||
@ -85,6 +85,7 @@
|
||||
"restTime": "Overgebleven {{time}}"
|
||||
}
|
||||
},
|
||||
"fileQueueInfo": "Bezig met uploaden van de eerste {{count}} bestanden, de resterende {{remaining}} bestanden worden in de wachtrij geplaatst",
|
||||
"totalCount": "Totaal {{count}} items",
|
||||
"uploadStatus": {
|
||||
"error": "Uploadfout",
|
||||
|
||||
@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V is gebouwd op het GLM-4.5-Air basismodel, erft de bewezen technologie van GLM-4.1V-Thinking en realiseert efficiënte schaalvergroting via een krachtige MoE-architectuur met 106 miljard parameters."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -163,9 +163,9 @@
|
||||
"addMember": "Dodaj członka",
|
||||
"allMembers": "Wszyscy członkowie",
|
||||
"createGroup": "Utwórz zespół Agentów",
|
||||
"noAvailableAgents": "Brak dostępnych asystentów do zaproszenia",
|
||||
"noSelectedAgents": "Nie wybrano jeszcze asystentów",
|
||||
"searchAgents": "Szukaj asystentów...",
|
||||
"noAvailableAgents": "Brak dostępnych Agentów do zaproszenia",
|
||||
"noSelectedAgents": "Nie wybrano jeszcze żadnego Agenta",
|
||||
"searchAgents": "Szukaj Agenta...",
|
||||
"setInitialMembers": "Wybierz członków zespołu"
|
||||
},
|
||||
"members": "Członkowie",
|
||||
@ -229,7 +229,7 @@
|
||||
"jumpToMessage": "Przejdź do wiadomości nr {{index}}",
|
||||
"nextMessage": "Następna wiadomość",
|
||||
"previousMessage": "Poprzednia wiadomość",
|
||||
"senderAssistant": "Asystent",
|
||||
"senderAssistant": "Agent",
|
||||
"senderUser": "Ty"
|
||||
},
|
||||
"newAgent": "Nowy asystent",
|
||||
|
||||
@ -85,6 +85,7 @@
|
||||
"restTime": "Pozostały czas {{time}}"
|
||||
}
|
||||
},
|
||||
"fileQueueInfo": "Trwa przesyłanie {{count}} pierwszych plików, pozostałe {{remaining}} pliki zostaną dodane do kolejki",
|
||||
"totalCount": "Łącznie {{count}} pozycji",
|
||||
"uploadStatus": {
|
||||
"error": "Błąd przesyłania",
|
||||
|
||||
@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V zbudowany jest na bazie GLM-4.5-Air, dziedzicząc zweryfikowane technologie GLM-4.1V-Thinking, jednocześnie skutecznie skalując się dzięki potężnej architekturze MoE z 106 miliardami parametrów."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -163,9 +163,9 @@
|
||||
"addMember": "Adicionar membro",
|
||||
"allMembers": "Todos os membros",
|
||||
"createGroup": "Criar time de Agentes",
|
||||
"noAvailableAgents": "Nenhum assistente disponível para convidar",
|
||||
"noSelectedAgents": "Nenhum assistente selecionado ainda",
|
||||
"searchAgents": "Pesquisar assistentes...",
|
||||
"noAvailableAgents": "Nenhum agente disponível para convite",
|
||||
"noSelectedAgents": "Nenhum agente selecionado ainda",
|
||||
"searchAgents": "Buscar agentes...",
|
||||
"setInitialMembers": "Selecionar membros do time"
|
||||
},
|
||||
"members": "Membros",
|
||||
@ -229,7 +229,7 @@
|
||||
"jumpToMessage": "Ir para a mensagem nº {{index}}",
|
||||
"nextMessage": "Próxima mensagem",
|
||||
"previousMessage": "Mensagem anterior",
|
||||
"senderAssistant": "Assistente",
|
||||
"senderAssistant": "Agente",
|
||||
"senderUser": "Você"
|
||||
},
|
||||
"newAgent": "Novo Assistente",
|
||||
|
||||
@ -85,6 +85,7 @@
|
||||
"restTime": "Restante {{time}}"
|
||||
}
|
||||
},
|
||||
"fileQueueInfo": "Enviando os primeiros {{count}} arquivos, os {{remaining}} restantes estão na fila para upload",
|
||||
"totalCount": "Total de {{count}} itens",
|
||||
"uploadStatus": {
|
||||
"error": "Erro no envio",
|
||||
|
||||
@ -163,9 +163,9 @@
|
||||
"addMember": "Добавить участника",
|
||||
"allMembers": "Все участники",
|
||||
"createGroup": "Создать команду агентов",
|
||||
"noAvailableAgents": "Нет доступных помощников для приглашения",
|
||||
"noSelectedAgents": "Помощники не выбраны",
|
||||
"searchAgents": "Поиск помощников...",
|
||||
"noAvailableAgents": "Нет доступных агентов для приглашения",
|
||||
"noSelectedAgents": "Агенты ещё не выбраны",
|
||||
"searchAgents": "Поиск агентов...",
|
||||
"setInitialMembers": "Выберите участников команды"
|
||||
},
|
||||
"members": "Участники",
|
||||
@ -229,7 +229,7 @@
|
||||
"jumpToMessage": "Перейти к сообщению № {{index}}",
|
||||
"nextMessage": "Следующее сообщение",
|
||||
"previousMessage": "Предыдущее сообщение",
|
||||
"senderAssistant": "Ассистент",
|
||||
"senderAssistant": "Агент",
|
||||
"senderUser": "Вы"
|
||||
},
|
||||
"newAgent": "Создать помощника",
|
||||
|
||||
@ -85,6 +85,7 @@
|
||||
"restTime": "Осталось {{time}}"
|
||||
}
|
||||
},
|
||||
"fileQueueInfo": "Загружаются первые {{count}} файлов, оставшиеся {{remaining}} будут поставлены в очередь",
|
||||
"totalCount": "Всего {{count}} элементов",
|
||||
"uploadStatus": {
|
||||
"error": "Ошибка загрузки",
|
||||
|
||||
@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V построена на базе GLM-4.5-Air, наследуя проверенные технологии GLM-4.1V-Thinking и обеспечивая эффективное масштабирование благодаря мощной архитектуре MoE с 106 миллиардами параметров."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -163,9 +163,9 @@
|
||||
"addMember": "Üye Ekle",
|
||||
"allMembers": "Tüm üyeler",
|
||||
"createGroup": "Agent Ekibi Oluştur",
|
||||
"noAvailableAgents": "Davet edilecek asistan yok",
|
||||
"noSelectedAgents": "Henüz asistan seçilmedi",
|
||||
"searchAgents": "Asistan ara...",
|
||||
"noAvailableAgents": "Davet edilebilecek bir Agent yok",
|
||||
"noSelectedAgents": "Henüz bir Agent seçilmedi",
|
||||
"searchAgents": "Agent ara...",
|
||||
"setInitialMembers": "Ekip üyelerini seç"
|
||||
},
|
||||
"members": "Üyeler",
|
||||
@ -229,7 +229,7 @@
|
||||
"jumpToMessage": "{{index}} numaralı mesaja atla",
|
||||
"nextMessage": "Sonraki mesaj",
|
||||
"previousMessage": "Önceki mesaj",
|
||||
"senderAssistant": "Asistan",
|
||||
"senderAssistant": "Agent",
|
||||
"senderUser": "Sen"
|
||||
},
|
||||
"newAgent": "Yeni Asistan",
|
||||
|
||||
@ -85,6 +85,7 @@
|
||||
"restTime": "Kalan {{time}}"
|
||||
}
|
||||
},
|
||||
"fileQueueInfo": "{{count}} dosya yükleniyor, kalan {{remaining}} dosya sıraya alınacak",
|
||||
"totalCount": "Toplam {{count}} öğe",
|
||||
"uploadStatus": {
|
||||
"error": "Yükleme hatası",
|
||||
|
||||
@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V, GLM-4.5-Air temel modeli üzerine inşa edilmiştir, GLM-4.1V-Thinking'in doğrulanmış teknolojisini devralır ve güçlü 106 milyar parametreli MoE mimarisi ile etkili ölçeklenebilirlik sağlar."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -163,9 +163,9 @@
|
||||
"addMember": "Thêm thành viên",
|
||||
"allMembers": "Tất cả thành viên",
|
||||
"createGroup": "Tạo nhóm Agent",
|
||||
"noAvailableAgents": "Không có trợ lý nào để mời",
|
||||
"noSelectedAgents": "Chưa chọn trợ lý nào",
|
||||
"searchAgents": "Tìm trợ lý...",
|
||||
"noAvailableAgents": "Không có Agent nào để mời",
|
||||
"noSelectedAgents": "Chưa chọn Agent nào",
|
||||
"searchAgents": "Tìm kiếm Agent...",
|
||||
"setInitialMembers": "Chọn thành viên nhóm"
|
||||
},
|
||||
"members": "Thành viên",
|
||||
@ -229,7 +229,7 @@
|
||||
"jumpToMessage": "Chuyển đến tin nhắn thứ {{index}}",
|
||||
"nextMessage": "Tin nhắn tiếp theo",
|
||||
"previousMessage": "Tin nhắn trước",
|
||||
"senderAssistant": "Trợ lý",
|
||||
"senderAssistant": "Agent",
|
||||
"senderUser": "Bạn"
|
||||
},
|
||||
"newAgent": "Tạo trợ lý mới",
|
||||
|
||||
@ -85,6 +85,7 @@
|
||||
"restTime": "Thời gian còn lại {{time}}"
|
||||
}
|
||||
},
|
||||
"fileQueueInfo": "Đang tải lên {{count}} tệp đầu tiên, còn lại {{remaining}} tệp sẽ được xếp hàng để tải lên",
|
||||
"totalCount": "Tổng cộng {{count}} mục",
|
||||
"uploadStatus": {
|
||||
"error": "Lỗi tải lên",
|
||||
|
||||
@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V được xây dựng trên mô hình nền tảng GLM-4.5-Air, kế thừa công nghệ đã được xác minh của GLM-4.1V-Thinking, đồng thời mở rộng hiệu quả với kiến trúc MoE 106 tỷ tham số mạnh mẽ."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -163,9 +163,9 @@
|
||||
"addMember": "添加成员",
|
||||
"allMembers": "全体成员",
|
||||
"createGroup": "创建 Agent 团队",
|
||||
"noAvailableAgents": "没有可邀请的助手",
|
||||
"noSelectedAgents": "还未选择助手",
|
||||
"searchAgents": "搜索助手...",
|
||||
"noAvailableAgents": "没有可邀请的 Agent",
|
||||
"noSelectedAgents": "还未选择 Agent",
|
||||
"searchAgents": "搜索 Agent...",
|
||||
"setInitialMembers": "选择团队成员"
|
||||
},
|
||||
"members": "Members",
|
||||
@ -229,7 +229,7 @@
|
||||
"jumpToMessage": "跳转至第 {{index}} 条消息",
|
||||
"nextMessage": "下一条消息",
|
||||
"previousMessage": "上一条消息",
|
||||
"senderAssistant": "助手",
|
||||
"senderAssistant": "Agent",
|
||||
"senderUser": "你"
|
||||
},
|
||||
"newAgent": "新建助手",
|
||||
|
||||
@ -85,6 +85,7 @@
|
||||
"restTime": "剩余 {{time}}"
|
||||
}
|
||||
},
|
||||
"fileQueueInfo": "正在上传前 {{count}} 个文件,剩余 {{remaining}} 个文件将排队上传",
|
||||
"totalCount": "共 {{count}} 项",
|
||||
"uploadStatus": {
|
||||
"error": "上传出错",
|
||||
|
||||
@ -163,9 +163,9 @@
|
||||
"addMember": "添加成員",
|
||||
"allMembers": "所有成員",
|
||||
"createGroup": "建立 Agent 團隊",
|
||||
"noAvailableAgents": "沒有可邀請的助理",
|
||||
"noSelectedAgents": "尚未選擇助理",
|
||||
"searchAgents": "搜尋助理...",
|
||||
"noAvailableAgents": "沒有可邀請的 Agent",
|
||||
"noSelectedAgents": "尚未選擇 Agent",
|
||||
"searchAgents": "搜尋 Agent...",
|
||||
"setInitialMembers": "選擇團隊成員"
|
||||
},
|
||||
"members": "成員",
|
||||
@ -229,7 +229,7 @@
|
||||
"jumpToMessage": "跳轉至第 {{index}} 條訊息",
|
||||
"nextMessage": "下一條訊息",
|
||||
"previousMessage": "上一條訊息",
|
||||
"senderAssistant": "助理",
|
||||
"senderAssistant": "Agent",
|
||||
"senderUser": "您"
|
||||
},
|
||||
"newAgent": "新建助手",
|
||||
|
||||
@ -85,6 +85,7 @@
|
||||
"restTime": "剩餘 {{time}}"
|
||||
}
|
||||
},
|
||||
"fileQueueInfo": "正在上傳前 {{count}} 個檔案,剩餘 {{remaining}} 個檔案將排隊上傳",
|
||||
"totalCount": "共 {{count}} 項",
|
||||
"uploadStatus": {
|
||||
"error": "上傳出錯",
|
||||
|
||||
@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V 基於 GLM-4.5-Air 基礎模型構建,繼承了 GLM-4.1V-Thinking 的經過驗證的技術,同時透過強大的 1060 億參數 MoE 架構實現了有效的擴展。"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -208,6 +208,7 @@
|
||||
"drizzle-zod": "^0.5.1",
|
||||
"epub2": "^3.0.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fflate": "^0.8.2",
|
||||
"file-type": "^21.0.0",
|
||||
"framer-motion": "^12.23.24",
|
||||
"gpt-tokenizer": "^3.2.0",
|
||||
|
||||
@ -6,3 +6,5 @@ export const FILE_UPLOAD_BLACKLIST = [
|
||||
'ehthumbs.db',
|
||||
'ehthumbs_vista.db',
|
||||
];
|
||||
|
||||
export const MAX_UPLOAD_FILE_COUNT = 10;
|
||||
|
||||
@ -79,7 +79,7 @@ const useStyles = createStyles(({ css, token, cx, isDarkMode }) => {
|
||||
interface FileRenderItemProps extends FileListItem {
|
||||
index: number;
|
||||
knowledgeBaseId?: string;
|
||||
onSelectedChange: (id: string, selected: boolean) => void;
|
||||
onSelectedChange: (id: string, selected: boolean, shiftKey: boolean, index: number) => void;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
@ -100,6 +100,7 @@ const FileRenderItem = memo<FileRenderItemProps>(
|
||||
chunkingStatus,
|
||||
onSelectedChange,
|
||||
knowledgeBaseId,
|
||||
index,
|
||||
}) => {
|
||||
const { t } = useTranslation('components');
|
||||
const { styles, cx } = useStyles();
|
||||
@ -140,7 +141,7 @@ const FileRenderItem = memo<FileRenderItemProps>(
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
onSelectedChange(id, !selected);
|
||||
onSelectedChange(id, !selected, e.shiftKey, index);
|
||||
}}
|
||||
style={{ paddingInline: 4 }}
|
||||
>
|
||||
|
||||
@ -21,7 +21,7 @@ const MasonryItemWrapper = memo<MasonryItemWrapperProps>(({ data: item, context
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '8px' }}>
|
||||
<div style={{ padding: '8px 4px' }}>
|
||||
<MasonryFileItem
|
||||
knowledgeBaseId={context.knowledgeBaseId}
|
||||
onSelectedChange={(id, checked) => {
|
||||
|
||||
@ -111,8 +111,6 @@ const useStyles = createStyles(({ css, token }) => ({
|
||||
inset-block-end: 8px;
|
||||
inset-inline-end: 8px;
|
||||
|
||||
padding-block: 4px;
|
||||
padding-inline: 8px;
|
||||
border-radius: ${token.borderRadius}px;
|
||||
|
||||
opacity: 0;
|
||||
@ -320,8 +318,8 @@ const MasonryFileItem = memo<MasonryFileItemProps>(
|
||||
});
|
||||
},
|
||||
{
|
||||
rootMargin: '50px', // Start loading slightly before entering viewport
|
||||
threshold: 0.1,
|
||||
rootMargin: '200px', // Increased margin to load content earlier
|
||||
threshold: 0.01, // Lower threshold for earlier triggering
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -26,6 +26,9 @@ const MasonrySkeleton = memo<MasonrySkeletonProps>(({ columnCount }) => {
|
||||
// Generate varying heights for more natural masonry look
|
||||
const heights = [180, 220, 200, 190, 240, 210, 200, 230, 180, 220, 210, 190];
|
||||
|
||||
// Calculate number of items based on viewport and column count
|
||||
const itemCount = Math.min(columnCount * 3, 12);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.grid}
|
||||
@ -33,18 +36,21 @@ const MasonrySkeleton = memo<MasonrySkeletonProps>(({ columnCount }) => {
|
||||
gridTemplateColumns: `repeat(${columnCount}, 1fr)`,
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: 12 }).map((_, index) => (
|
||||
{Array.from({ length: itemCount }).map((_, index) => (
|
||||
<div className={styles.card} key={index}>
|
||||
<Skeleton
|
||||
active
|
||||
avatar={false}
|
||||
paragraph={{
|
||||
rows: 3,
|
||||
width: ['100%', '80%', '60%'],
|
||||
rows: 4,
|
||||
width: ['100%', '90%', '70%', '50%'],
|
||||
}}
|
||||
style={{
|
||||
height: heights[index],
|
||||
height: heights[index % heights.length],
|
||||
}}
|
||||
title={{
|
||||
width: '100%',
|
||||
}}
|
||||
title={false}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@ -148,7 +148,7 @@ const MultiSelectActions = memo<MultiSelectActionsProps>(
|
||||
size={'small'}
|
||||
variant={'filled'}
|
||||
>
|
||||
{t('batchDelete', { ns: 'common' })}
|
||||
{t('delete', { ns: 'common' })}
|
||||
</Button>
|
||||
</Flexbox>
|
||||
)}
|
||||
|
||||
@ -51,10 +51,15 @@ const FileList = memo<FileListProps>(({ knowledgeBaseId, category }) => {
|
||||
|
||||
const [selectFileIds, setSelectedFileIds] = useState<string[]>([]);
|
||||
const [viewConfig, setViewConfig] = useState({ showFilesInKnowledgeBase: false });
|
||||
const [lastSelectedIndex, setLastSelectedIndex] = useState<number | null>(null);
|
||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||
|
||||
const viewMode = useGlobalStore((s) => s.status.fileManagerViewMode || 'list') as ViewMode;
|
||||
const updateSystemStatus = useGlobalStore((s) => s.updateSystemStatus);
|
||||
const setViewMode = (mode: ViewMode) => updateSystemStatus({ fileManagerViewMode: mode });
|
||||
const setViewMode = (mode: ViewMode) => {
|
||||
setIsTransitioning(true);
|
||||
updateSystemStatus({ fileManagerViewMode: mode });
|
||||
};
|
||||
|
||||
const [columnCount, setColumnCount] = useState(4);
|
||||
|
||||
@ -105,6 +110,19 @@ const FileList = memo<FileListProps>(({ knowledgeBaseId, category }) => {
|
||||
...viewConfig,
|
||||
});
|
||||
|
||||
// Handle view transition with a brief delay to show skeleton
|
||||
React.useEffect(() => {
|
||||
if (isTransitioning && data) {
|
||||
// Use requestAnimationFrame to ensure smooth transition
|
||||
requestAnimationFrame(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setIsTransitioning(false);
|
||||
}, 100);
|
||||
return () => clearTimeout(timer);
|
||||
});
|
||||
}
|
||||
}, [isTransitioning, viewMode, data]);
|
||||
|
||||
useCheckTaskStatus(data);
|
||||
|
||||
// Clean up selected files that no longer exist in the data
|
||||
@ -118,6 +136,13 @@ const FileList = memo<FileListProps>(({ knowledgeBaseId, category }) => {
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
// Reset lastSelectedIndex when selection is cleared
|
||||
React.useEffect(() => {
|
||||
if (selectFileIds.length === 0) {
|
||||
setLastSelectedIndex(null);
|
||||
}
|
||||
}, [selectFileIds.length]);
|
||||
|
||||
// Memoize context object to avoid recreating on every render
|
||||
const masonryContext = useMemo(
|
||||
() => ({
|
||||
@ -161,7 +186,7 @@ const FileList = memo<FileListProps>(({ knowledgeBaseId, category }) => {
|
||||
</Flexbox>
|
||||
)}
|
||||
</Flexbox>
|
||||
{isLoading ? (
|
||||
{isLoading || isTransitioning ? (
|
||||
viewMode === 'masonry' ? (
|
||||
<MasonrySkeleton columnCount={columnCount} />
|
||||
) : (
|
||||
@ -184,13 +209,30 @@ const FileList = memo<FileListProps>(({ knowledgeBaseId, category }) => {
|
||||
index={index}
|
||||
key={item.id}
|
||||
knowledgeBaseId={knowledgeBaseId}
|
||||
onSelectedChange={(id, checked) => {
|
||||
setSelectedFileIds((prev) => {
|
||||
if (checked) {
|
||||
return [...prev, id];
|
||||
}
|
||||
return prev.filter((item) => item !== id);
|
||||
});
|
||||
onSelectedChange={(id, checked, shiftKey, clickedIndex) => {
|
||||
if (shiftKey && lastSelectedIndex !== null && selectFileIds.length > 0 && data) {
|
||||
// Range selection with shift key
|
||||
const start = Math.min(lastSelectedIndex, clickedIndex);
|
||||
const end = Math.max(lastSelectedIndex, clickedIndex);
|
||||
const rangeIds = data.slice(start, end + 1).map((item) => item.id);
|
||||
|
||||
setSelectedFileIds((prev) => {
|
||||
// Create a Set for efficient lookup
|
||||
const prevSet = new Set(prev);
|
||||
// Add all items in range
|
||||
rangeIds.forEach((rangeId) => prevSet.add(rangeId));
|
||||
return Array.from(prevSet);
|
||||
});
|
||||
} else {
|
||||
// Normal selection
|
||||
setSelectedFileIds((prev) => {
|
||||
if (checked) {
|
||||
return [...prev, id];
|
||||
}
|
||||
return prev.filter((item) => item !== id);
|
||||
});
|
||||
}
|
||||
setLastSelectedIndex(clickedIndex);
|
||||
}}
|
||||
selected={selectFileIds.includes(item.id)}
|
||||
{...item}
|
||||
|
||||
@ -175,9 +175,9 @@ export default {
|
||||
addMember: '添加成员',
|
||||
allMembers: '全体成员',
|
||||
createGroup: '创建 Agent 团队',
|
||||
noAvailableAgents: '没有可邀请的助手',
|
||||
noSelectedAgents: '还未选择助手',
|
||||
searchAgents: '搜索助手...',
|
||||
noAvailableAgents: '没有可邀请的 Agent',
|
||||
noSelectedAgents: '还未选择 Agent',
|
||||
searchAgents: '搜索 Agent...',
|
||||
setInitialMembers: '选择团队成员',
|
||||
},
|
||||
|
||||
@ -247,7 +247,7 @@ export default {
|
||||
jumpToMessage: '跳转至第 {{index}} 条消息',
|
||||
nextMessage: '下一条消息',
|
||||
previousMessage: '上一条消息',
|
||||
senderAssistant: '助手',
|
||||
senderAssistant: 'Agent',
|
||||
senderUser: '你',
|
||||
},
|
||||
|
||||
|
||||
@ -86,6 +86,7 @@ export default {
|
||||
restTime: '剩余 {{time}}',
|
||||
},
|
||||
},
|
||||
fileQueueInfo: '正在上传前 {{count}} 个文件,剩余 {{remaining}} 个文件将排队上传',
|
||||
totalCount: '共 {{count}} 项',
|
||||
uploadStatus: {
|
||||
error: '上传出错',
|
||||
|
||||
@ -2,17 +2,50 @@ import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { mutate } from 'swr';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { FILE_UPLOAD_BLACKLIST } from '@/const/file';
|
||||
import { message } from '@/components/AntdStaticMethods';
|
||||
import { FILE_UPLOAD_BLACKLIST, MAX_UPLOAD_FILE_COUNT } from '@/const/file';
|
||||
import { lambdaClient } from '@/libs/trpc/client';
|
||||
import { fileService } from '@/services/file';
|
||||
import { ragService } from '@/services/rag';
|
||||
import { FileListItem } from '@/types/files';
|
||||
import { UploadFileItem } from '@/types/files/upload';
|
||||
import { unzipFile } from '@/utils/unzipFile';
|
||||
|
||||
import { useFileStore as useStore } from '../../store';
|
||||
|
||||
vi.mock('zustand/traditional');
|
||||
|
||||
// Mock i18next translation function
|
||||
vi.mock('i18next', () => ({
|
||||
t: (key: string, options?: any) => {
|
||||
// Return a mock translation string that includes the options for verification
|
||||
if (key === 'uploadDock.fileQueueInfo' && options?.count !== undefined) {
|
||||
return `Uploading ${options.count} files, ${options.remaining} queued`;
|
||||
}
|
||||
return key;
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock message
|
||||
vi.mock('@/components/AntdStaticMethods', () => ({
|
||||
message: {
|
||||
info: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock unzipFile
|
||||
vi.mock('@/utils/unzipFile', () => ({
|
||||
unzipFile: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock p-map to run sequentially for easier testing
|
||||
vi.mock('p-map', () => ({
|
||||
default: vi.fn(async (items, mapper) => {
|
||||
return Promise.all(items.map(mapper));
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock SWR
|
||||
vi.mock('swr', async () => {
|
||||
const actual = await vi.importActual('swr');
|
||||
@ -398,6 +431,108 @@ describe('FileManagerActions', () => {
|
||||
// Should not auto-parse when upload returns undefined
|
||||
expect(parseSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should enforce file count limit and queue excess files', async () => {
|
||||
const { result } = renderHook(() => useStore());
|
||||
|
||||
// Create more files than the limit
|
||||
const totalFiles = MAX_UPLOAD_FILE_COUNT + 5;
|
||||
const files = Array.from(
|
||||
{ length: totalFiles },
|
||||
(_, i) => new File(['content'], `file-${i}.txt`, { type: 'text/plain' }),
|
||||
);
|
||||
|
||||
vi.spyOn(result.current, 'uploadWithProgress').mockResolvedValue({
|
||||
id: 'file-1',
|
||||
url: 'http://example.com/file-1',
|
||||
});
|
||||
vi.spyOn(result.current, 'refreshFileList').mockResolvedValue();
|
||||
vi.spyOn(result.current, 'parseFilesToChunks').mockResolvedValue();
|
||||
const dispatchSpy = vi.spyOn(result.current, 'dispatchDockFileList');
|
||||
|
||||
await act(async () => {
|
||||
await result.current.pushDockFileList(files);
|
||||
});
|
||||
|
||||
// Should add all files to dock (not just first MAX_UPLOAD_FILE_COUNT)
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
atStart: true,
|
||||
files: expect.arrayContaining([
|
||||
expect.objectContaining({ file: expect.any(File), status: 'pending' }),
|
||||
]),
|
||||
type: 'addFiles',
|
||||
});
|
||||
|
||||
// Verify all files were dispatched
|
||||
const dispatchCall = dispatchSpy.mock.calls.find((call) => call[0].type === 'addFiles');
|
||||
expect(dispatchCall?.[0]).toHaveProperty('files');
|
||||
if (dispatchCall && 'files' in dispatchCall[0]) {
|
||||
expect(dispatchCall[0].files).toHaveLength(totalFiles);
|
||||
}
|
||||
});
|
||||
|
||||
it('should extract ZIP files and upload contents', async () => {
|
||||
const { result } = renderHook(() => useStore());
|
||||
|
||||
const zipFile = new File(['zip content'], 'archive.zip', { type: 'application/zip' });
|
||||
const extractedFiles = [
|
||||
new File(['file1'], 'file1.txt', { type: 'text/plain' }),
|
||||
new File(['file2'], 'file2.txt', { type: 'text/plain' }),
|
||||
];
|
||||
|
||||
vi.mocked(unzipFile).mockResolvedValue(extractedFiles);
|
||||
vi.spyOn(result.current, 'uploadWithProgress').mockResolvedValue({
|
||||
id: 'file-1',
|
||||
url: 'http://example.com/file-1',
|
||||
});
|
||||
vi.spyOn(result.current, 'refreshFileList').mockResolvedValue();
|
||||
vi.spyOn(result.current, 'parseFilesToChunks').mockResolvedValue();
|
||||
const dispatchSpy = vi.spyOn(result.current, 'dispatchDockFileList');
|
||||
|
||||
await act(async () => {
|
||||
await result.current.pushDockFileList([zipFile]);
|
||||
});
|
||||
|
||||
// Should extract ZIP file
|
||||
expect(unzipFile).toHaveBeenCalledWith(zipFile);
|
||||
|
||||
// Should upload extracted files
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
atStart: true,
|
||||
files: extractedFiles.map((file) => ({ file, id: file.name, status: 'pending' })),
|
||||
type: 'addFiles',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle ZIP extraction errors gracefully', async () => {
|
||||
const { result } = renderHook(() => useStore());
|
||||
|
||||
const zipFile = new File(['zip content'], 'archive.zip', { type: 'application/zip' });
|
||||
|
||||
vi.mocked(unzipFile).mockRejectedValue(new Error('Extraction failed'));
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
vi.spyOn(result.current, 'uploadWithProgress').mockResolvedValue({
|
||||
id: 'file-1',
|
||||
url: 'http://example.com/file-1',
|
||||
});
|
||||
vi.spyOn(result.current, 'refreshFileList').mockResolvedValue();
|
||||
vi.spyOn(result.current, 'parseFilesToChunks').mockResolvedValue();
|
||||
const dispatchSpy = vi.spyOn(result.current, 'dispatchDockFileList');
|
||||
|
||||
await act(async () => {
|
||||
await result.current.pushDockFileList([zipFile]);
|
||||
});
|
||||
|
||||
// Should log error
|
||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||
|
||||
// Should fallback to uploading the ZIP file itself
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
atStart: true,
|
||||
files: [{ file: zipFile, id: zipFile.name, status: 'pending' }],
|
||||
type: 'addFiles',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reEmbeddingChunks', () => {
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import pMap from 'p-map';
|
||||
import { SWRResponse, mutate } from 'swr';
|
||||
import { StateCreator } from 'zustand/vanilla';
|
||||
|
||||
import { FILE_UPLOAD_BLACKLIST } from '@/const/file';
|
||||
import { FILE_UPLOAD_BLACKLIST, MAX_UPLOAD_FILE_COUNT } from '@/const/file';
|
||||
import { useClientDataSWR } from '@/libs/swr';
|
||||
import { fileService } from '@/services/file';
|
||||
import { ServerService } from '@/services/file/server';
|
||||
@ -12,6 +13,7 @@ import {
|
||||
} from '@/store/file/reducers/uploadFileList';
|
||||
import { FileListItem, QueryFileListParams } from '@/types/files';
|
||||
import { isChunkingUnsupported } from '@/utils/isChunkingUnsupported';
|
||||
import { unzipFile } from '@/utils/unzipFile';
|
||||
|
||||
import { FileStore } from '../../store';
|
||||
import { fileManagerSelectors } from './selectors';
|
||||
@ -89,18 +91,37 @@ export const createFileManageSlice: StateCreator<
|
||||
pushDockFileList: async (rawFiles, knowledgeBaseId) => {
|
||||
const { dispatchDockFileList } = get();
|
||||
|
||||
// 0. skip file in blacklist
|
||||
const files = rawFiles.filter((file) => !FILE_UPLOAD_BLACKLIST.includes(file.name));
|
||||
// 0. Process ZIP files and extract their contents
|
||||
const filesToUpload: File[] = [];
|
||||
for (const file of rawFiles) {
|
||||
if (file.type === 'application/zip' || file.name.endsWith('.zip')) {
|
||||
try {
|
||||
const extractedFiles = await unzipFile(file);
|
||||
filesToUpload.push(...extractedFiles);
|
||||
} catch (error) {
|
||||
console.error('Failed to extract ZIP file:', error);
|
||||
// If extraction fails, treat it as a regular file
|
||||
filesToUpload.push(file);
|
||||
}
|
||||
} else {
|
||||
filesToUpload.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
// 1. add files
|
||||
// 1. skip file in blacklist
|
||||
const files = filesToUpload.filter((file) => !FILE_UPLOAD_BLACKLIST.includes(file.name));
|
||||
|
||||
// 2. Add all files to dock
|
||||
dispatchDockFileList({
|
||||
atStart: true,
|
||||
files: files.map((file) => ({ file, id: file.name, status: 'pending' })),
|
||||
type: 'addFiles',
|
||||
});
|
||||
|
||||
const uploadResults = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
// 3. Upload files with concurrency limit using p-map
|
||||
const uploadResults = await pMap(
|
||||
files,
|
||||
async (file) => {
|
||||
const result = await get().uploadWithProgress({
|
||||
file,
|
||||
knowledgeBaseId,
|
||||
@ -110,10 +131,11 @@ export const createFileManageSlice: StateCreator<
|
||||
await get().refreshFileList();
|
||||
|
||||
return { file, fileId: result?.id, fileType: file.type };
|
||||
}),
|
||||
},
|
||||
{ concurrency: MAX_UPLOAD_FILE_COUNT },
|
||||
);
|
||||
|
||||
// 2. auto-embed files that support chunking
|
||||
// 4. auto-embed files that support chunking
|
||||
const fileIdsToEmbed = uploadResults
|
||||
.filter(({ fileType, fileId }) => fileId && !isChunkingUnsupported(fileType))
|
||||
.map(({ fileId }) => fileId!);
|
||||
|
||||
128
src/utils/unzipFile.test.ts
Normal file
128
src/utils/unzipFile.test.ts
Normal file
@ -0,0 +1,128 @@
|
||||
import { zip } from 'fflate';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { unzipFile } from './unzipFile';
|
||||
|
||||
describe('unzipFile', () => {
|
||||
it('should extract files from a ZIP archive', async () => {
|
||||
// Create a mock ZIP file with test data
|
||||
const testFiles = {
|
||||
'test.txt': new TextEncoder().encode('Hello, World!'),
|
||||
'folder/nested.txt': new TextEncoder().encode('Nested file content'),
|
||||
};
|
||||
|
||||
const zipped = await new Promise<Uint8Array>((resolve, reject) => {
|
||||
zip(testFiles, (error, data) => {
|
||||
if (error) reject(error);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
const zipFile = new File([new Uint8Array(zipped)], 'test.zip', { type: 'application/zip' });
|
||||
|
||||
const extractedFiles = await unzipFile(zipFile);
|
||||
|
||||
expect(extractedFiles).toHaveLength(2);
|
||||
expect(extractedFiles[0].name).toBe('test.txt');
|
||||
expect(extractedFiles[1].name).toBe('nested.txt');
|
||||
|
||||
// Verify file contents
|
||||
const content1 = await extractedFiles[0].text();
|
||||
expect(content1).toBe('Hello, World!');
|
||||
|
||||
const content2 = await extractedFiles[1].text();
|
||||
expect(content2).toBe('Nested file content');
|
||||
});
|
||||
|
||||
it('should skip directories in ZIP archive', async () => {
|
||||
const testFiles = {
|
||||
'file.txt': new TextEncoder().encode('File content'),
|
||||
'folder/': new Uint8Array(0), // Directory entry
|
||||
};
|
||||
|
||||
const zipped = await new Promise<Uint8Array>((resolve, reject) => {
|
||||
zip(testFiles, (error, data) => {
|
||||
if (error) reject(error);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
const zipFile = new File([new Uint8Array(zipped)], 'test.zip', { type: 'application/zip' });
|
||||
|
||||
const extractedFiles = await unzipFile(zipFile);
|
||||
|
||||
expect(extractedFiles).toHaveLength(1);
|
||||
expect(extractedFiles[0].name).toBe('file.txt');
|
||||
});
|
||||
|
||||
it('should skip hidden files and __MACOSX directories', async () => {
|
||||
const testFiles = {
|
||||
'.hidden': new TextEncoder().encode('Hidden file'),
|
||||
'__MACOSX/._file.txt': new TextEncoder().encode('Mac metadata'),
|
||||
'visible.txt': new TextEncoder().encode('Visible file'),
|
||||
};
|
||||
|
||||
const zipped = await new Promise<Uint8Array>((resolve, reject) => {
|
||||
zip(testFiles, (error, data) => {
|
||||
if (error) reject(error);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
const zipFile = new File([new Uint8Array(zipped)], 'test.zip', { type: 'application/zip' });
|
||||
|
||||
const extractedFiles = await unzipFile(zipFile);
|
||||
|
||||
expect(extractedFiles).toHaveLength(1);
|
||||
expect(extractedFiles[0].name).toBe('visible.txt');
|
||||
});
|
||||
|
||||
it('should set correct MIME types for extracted files', async () => {
|
||||
const testFiles = {
|
||||
'document.pdf': new TextEncoder().encode('PDF content'),
|
||||
'image.png': new TextEncoder().encode('PNG content'),
|
||||
'code.ts': new TextEncoder().encode('TypeScript code'),
|
||||
};
|
||||
|
||||
const zipped = await new Promise<Uint8Array>((resolve, reject) => {
|
||||
zip(testFiles, (error, data) => {
|
||||
if (error) reject(error);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
const zipFile = new File([new Uint8Array(zipped)], 'test.zip', { type: 'application/zip' });
|
||||
|
||||
const extractedFiles = await unzipFile(zipFile);
|
||||
|
||||
expect(extractedFiles).toHaveLength(3);
|
||||
expect(extractedFiles.find((f) => f.name === 'document.pdf')?.type).toBe('application/pdf');
|
||||
expect(extractedFiles.find((f) => f.name === 'image.png')?.type).toBe('image/png');
|
||||
expect(extractedFiles.find((f) => f.name === 'code.ts')?.type).toBe('text/typescript');
|
||||
});
|
||||
|
||||
it('should handle empty ZIP files', async () => {
|
||||
const testFiles = {};
|
||||
|
||||
const zipped = await new Promise<Uint8Array>((resolve, reject) => {
|
||||
zip(testFiles, (error, data) => {
|
||||
if (error) reject(error);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
const zipFile = new File([new Uint8Array(zipped)], 'empty.zip', { type: 'application/zip' });
|
||||
|
||||
const extractedFiles = await unzipFile(zipFile);
|
||||
|
||||
expect(extractedFiles).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should reject on invalid ZIP file', async () => {
|
||||
const invalidFile = new File([new Uint8Array([1, 2, 3, 4])], 'invalid.zip', {
|
||||
type: 'application/zip',
|
||||
});
|
||||
|
||||
await expect(unzipFile(invalidFile)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
122
src/utils/unzipFile.ts
Normal file
122
src/utils/unzipFile.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import { unzip } from 'fflate';
|
||||
|
||||
/**
|
||||
* Determines the MIME type based on file extension
|
||||
*/
|
||||
const getFileType = (fileName: string): string => {
|
||||
const extension = fileName.split('.').pop()?.toLowerCase() || '';
|
||||
|
||||
const mimeTypes: Record<string, string> = {
|
||||
// Images
|
||||
bmp: 'image/bmp',
|
||||
|
||||
// Code files
|
||||
c: 'text/x-c',
|
||||
|
||||
cpp: 'text/x-c++',
|
||||
|
||||
cs: 'text/x-csharp',
|
||||
|
||||
css: 'text/css',
|
||||
|
||||
// Documents
|
||||
csv: 'text/csv',
|
||||
|
||||
doc: 'application/msword',
|
||||
|
||||
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
|
||||
gif: 'image/gif',
|
||||
|
||||
go: 'text/x-go',
|
||||
|
||||
html: 'text/html',
|
||||
|
||||
java: 'text/x-java',
|
||||
|
||||
jpeg: 'image/jpeg',
|
||||
|
||||
jpg: 'image/jpeg',
|
||||
|
||||
js: 'text/javascript',
|
||||
|
||||
json: 'application/json',
|
||||
|
||||
jsx: 'text/javascript',
|
||||
|
||||
md: 'text/markdown',
|
||||
|
||||
pdf: 'application/pdf',
|
||||
|
||||
php: 'application/x-httpd-php',
|
||||
|
||||
png: 'image/png',
|
||||
|
||||
ppt: 'application/vnd.ms-powerpoint',
|
||||
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
py: 'text/x-python',
|
||||
rb: 'text/x-ruby',
|
||||
rs: 'text/x-rust',
|
||||
rtf: 'application/rtf',
|
||||
sh: 'application/x-sh',
|
||||
svg: 'image/svg+xml',
|
||||
ts: 'text/typescript',
|
||||
tsx: 'text/typescript',
|
||||
txt: 'text/plain',
|
||||
webp: 'image/webp',
|
||||
xls: 'application/vnd.ms-excel',
|
||||
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
xml: 'application/xml',
|
||||
};
|
||||
|
||||
return mimeTypes[extension] || 'application/octet-stream';
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts files from a ZIP archive
|
||||
* @param zipFile - The ZIP file to extract
|
||||
* @returns Promise that resolves to an array of extracted Files
|
||||
*/
|
||||
export const unzipFile = async (zipFile: File): Promise<File[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
zipFile
|
||||
.arrayBuffer()
|
||||
.then((arrayBuffer) => {
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
|
||||
unzip(uint8Array, (error, unzipped) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const extractedFiles: File[] = [];
|
||||
|
||||
for (const [path, data] of Object.entries(unzipped)) {
|
||||
// Skip directories and hidden files
|
||||
if (path.endsWith('/') || path.includes('__MACOSX') || path.startsWith('.')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the filename from the path
|
||||
const fileName = path.split('/').pop() || path;
|
||||
|
||||
// Create a File object from the extracted data
|
||||
const blob = new Blob([new Uint8Array(data)], {
|
||||
type: getFileType(fileName),
|
||||
});
|
||||
const file = new File([blob], fileName, {
|
||||
type: getFileType(fileName),
|
||||
});
|
||||
|
||||
extractedFiles.push(file);
|
||||
}
|
||||
|
||||
resolve(extractedFiles);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
reject(new Error('Failed to read ZIP file'));
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -15,6 +15,7 @@ export default defineConfig({
|
||||
'@/const/locale': resolve(__dirname, './src/const/locale'),
|
||||
// TODO: after refactor the errorResponse, we can remove it
|
||||
'@/utils/errorResponse': resolve(__dirname, './src/utils/errorResponse'),
|
||||
'@/utils/unzipFile': resolve(__dirname, './src/utils/unzipFile'),
|
||||
'@/utils': resolve(__dirname, './packages/utils/src'),
|
||||
'@/types': resolve(__dirname, './packages/types/src'),
|
||||
'@/const': resolve(__dirname, './packages/const/src'),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user