💄 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:
René Wang 2025-10-27 10:25:50 +08:00 committed by GitHub
parent 1fe6a5997b
commit e568ce6f31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
66 changed files with 599 additions and 122 deletions

View File

@ -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": "مساعد جديد",

View File

@ -85,6 +85,7 @@
"restTime": "الوقت المتبقي {{time}}"
}
},
"fileQueueInfo": "يتم حاليًا تحميل {{count}} ملفًا، وسيتم وضع {{remaining}} ملفًا في قائمة الانتظار للتحميل",
"totalCount": "إجمالي {{count}} عنصر",
"uploadStatus": {
"error": "حدث خطأ أثناء الرفع",

View File

@ -3314,4 +3314,4 @@
"zai/glm-4.5v": {
"description": "GLM-4.5V مبني على نموذج GLM-4.5-Air الأساسي، يرث التقنيات المثبتة من GLM-4.1V-Thinking، ويوسعها بفعالية من خلال بنية MoE القوية التي تضم 106 مليار معلمة."
}
}
}

View File

@ -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": "Нов агент",

View File

@ -85,6 +85,7 @@
"restTime": "Остава {{time}}"
}
},
"fileQueueInfo": "Качват се първите {{count}} файла, останалите {{remaining}} ще бъдат поставени в опашка за качване",
"totalCount": "Общо {{count}} елемента",
"uploadStatus": {
"error": "Грешка при качване",

View File

@ -3314,4 +3314,4 @@
"zai/glm-4.5v": {
"description": "GLM-4.5V е изграден върху основния модел GLM-4.5-Air, наследявайки проверените технологии на GLM-4.1V-Thinking и постига ефективно мащабиране чрез мощната MoE архитектура с 106 милиарда параметри."
}
}
}

View File

@ -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",

View File

@ -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",

View File

@ -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."
}
}
}

View File

@ -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",

View File

@ -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",

View File

@ -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."
}
}
}

View File

@ -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",

View File

@ -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",

View File

@ -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."
}
}
}

View File

@ -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": "دستیار جدید",

View File

@ -85,6 +85,7 @@
"restTime": "زمان باقی‌مانده {{time}}"
}
},
"fileQueueInfo": "در حال بارگذاری {{count}} فایل اول، {{remaining}} فایل باقی‌مانده در صف بارگذاری قرار خواهند گرفت",
"totalCount": "مجموعاً {{count}} مورد",
"uploadStatus": {
"error": "خطا در بارگذاری",

View File

@ -3314,4 +3314,4 @@
"zai/glm-4.5v": {
"description": "GLM-4.5V بر پایه مدل پایه GLM-4.5-Air ساخته شده است، فناوری اثبات شده GLM-4.1V-Thinking را به ارث برده و در عین حال با معماری قدرتمند MoE با 106 میلیارد پارامتر به طور مؤثر مقیاس‌پذیر شده است."
}
}
}

View File

@ -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",

View File

@ -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 dattente",
"totalCount": "Total {{count}} éléments",
"uploadStatus": {
"error": "Erreur de téléchargement",

View File

@ -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."
}
}
}

View File

@ -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",

View File

@ -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",

View File

@ -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": "新しいエージェント",

View File

@ -85,6 +85,7 @@
"restTime": "残り {{time}}"
}
},
"fileQueueInfo": "最初の {{count}} 件のファイルをアップロード中、残りの {{remaining}} 件は順番待ちです",
"totalCount": "合計 {{count}} 件",
"uploadStatus": {
"error": "アップロードエラー",

View File

@ -3314,4 +3314,4 @@
"zai/glm-4.5v": {
"description": "GLM-4.5V は GLM-4.5-Air 基盤モデルに基づき、GLM-4.1V-Thinking の検証済み技術を継承しつつ、強力な1060億パラメータの MoE アーキテクチャで効率的にスケールアップしています。"
}
}
}

View File

@ -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": "새 도우미",

View File

@ -85,6 +85,7 @@
"restTime": "남은 시간 {{time}}"
}
},
"fileQueueInfo": "{{count}}개 파일을 업로드 중이며, 나머지 {{remaining}}개 파일은 대기 중입니다",
"totalCount": "총 {{count}}개 항목",
"uploadStatus": {
"error": "업로드 오류",

View File

@ -3314,4 +3314,4 @@
"zai/glm-4.5v": {
"description": "GLM-4.5V는 GLM-4.5-Air 기본 모델을 기반으로 구축되었으며, 검증된 GLM-4.1V-Thinking 기술을 계승하면서 강력한 1060억 매개변수 MoE 아키텍처를 통해 효율적인 확장을 실현했습니다."
}
}
}

View File

@ -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",

View File

@ -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",

View File

@ -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."
}
}
}

View File

@ -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",

View File

@ -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",

View File

@ -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."
}
}
}

View File

@ -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",

View File

@ -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",

View File

@ -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": "Создать помощника",

View File

@ -85,6 +85,7 @@
"restTime": "Осталось {{time}}"
}
},
"fileQueueInfo": "Загружаются первые {{count}} файлов, оставшиеся {{remaining}} будут поставлены в очередь",
"totalCount": "Всего {{count}} элементов",
"uploadStatus": {
"error": "Ошибка загрузки",

View File

@ -3314,4 +3314,4 @@
"zai/glm-4.5v": {
"description": "GLM-4.5V построена на базе GLM-4.5-Air, наследуя проверенные технологии GLM-4.1V-Thinking и обеспечивая эффективное масштабирование благодаря мощной архитектуре MoE с 106 миллиардами параметров."
}
}
}

View File

@ -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",

View File

@ -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ı",

View File

@ -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."
}
}
}

View File

@ -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",

View File

@ -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",

View File

@ -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ẽ."
}
}
}

View File

@ -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": "新建助手",

View File

@ -85,6 +85,7 @@
"restTime": "剩余 {{time}}"
}
},
"fileQueueInfo": "正在上传前 {{count}} 个文件,剩余 {{remaining}} 个文件将排队上传",
"totalCount": "共 {{count}} 项",
"uploadStatus": {
"error": "上传出错",

View File

@ -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": "新建助手",

View File

@ -85,6 +85,7 @@
"restTime": "剩餘 {{time}}"
}
},
"fileQueueInfo": "正在上傳前 {{count}} 個檔案,剩餘 {{remaining}} 個檔案將排隊上傳",
"totalCount": "共 {{count}} 項",
"uploadStatus": {
"error": "上傳出錯",

View File

@ -3314,4 +3314,4 @@
"zai/glm-4.5v": {
"description": "GLM-4.5V 基於 GLM-4.5-Air 基礎模型構建,繼承了 GLM-4.1V-Thinking 的經過驗證的技術,同時透過強大的 1060 億參數 MoE 架構實現了有效的擴展。"
}
}
}

View File

@ -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",

View File

@ -6,3 +6,5 @@ export const FILE_UPLOAD_BLACKLIST = [
'ehthumbs.db',
'ehthumbs_vista.db',
];
export const MAX_UPLOAD_FILE_COUNT = 10;

View File

@ -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 }}
>

View File

@ -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) => {

View File

@ -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
},
);

View File

@ -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>
))}

View File

@ -148,7 +148,7 @@ const MultiSelectActions = memo<MultiSelectActionsProps>(
size={'small'}
variant={'filled'}
>
{t('batchDelete', { ns: 'common' })}
{t('delete', { ns: 'common' })}
</Button>
</Flexbox>
)}

View File

@ -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}

View File

@ -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: '你',
},

View File

@ -86,6 +86,7 @@ export default {
restTime: '剩余 {{time}}',
},
},
fileQueueInfo: '正在上传前 {{count}} 个文件,剩余 {{remaining}} 个文件将排队上传',
totalCount: '共 {{count}} 项',
uploadStatus: {
error: '上传出错',

View File

@ -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', () => {

View File

@ -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
View 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
View 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'));
});
});
};

View 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'),