diff --git a/apps/client/package.json b/apps/client/package.json index 0cff05cc..9d45fa07 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -1,7 +1,7 @@ { "name": "client", "private": true, - "version": "0.23.2", + "version": "0.24.0", "scripts": { "dev": "vite", "build": "tsc && vite build", @@ -17,6 +17,7 @@ "@emoji-mart/react": "^1.1.1", "@excalidraw/excalidraw": "0.18.0-864353b", "@mantine/core": "^8.1.3", + "@mantine/dates": "^8.3.2", "@mantine/form": "^8.1.3", "@mantine/hooks": "^8.1.3", "@mantine/modals": "^8.1.3", @@ -26,7 +27,7 @@ "@tanstack/react-query": "^5.80.6", "@tiptap/extension-character-count": "^2.10.3", "alfaaz": "^1.1.0", - "axios": "^1.9.0", + "axios": "^1.13.2", "clsx": "^2.1.1", "emoji-mart": "^5.6.0", "file-saver": "^2.0.5", @@ -56,7 +57,7 @@ "socket.io-client": "^4.8.1", "tippy.js": "^6.3.7", "tiptap-extension-global-drag-handle": "^0.1.18", - "zod": "^3.25.56" + "zod": "^3.25.76" }, "devDependencies": { "@eslint/js": "^9.16.0", @@ -64,10 +65,10 @@ "@types/file-saver": "^2.0.7", "@types/js-cookie": "^3.0.6", "@types/katex": "^0.16.7", - "@types/node": "22.10.0", + "@types/node": "22.19.1", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react": "^4.4.1", + "@vitejs/plugin-react": "^5.1.1", "eslint": "^9.15.0", "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^5.1.0", @@ -80,6 +81,6 @@ "prettier": "^3.4.1", "typescript": "^5.7.2", "typescript-eslint": "^8.17.0", - "vite": "^6.3.5" + "vite": "^7.2.4" } } diff --git a/apps/client/public/locales/de-DE/translation.json b/apps/client/public/locales/de-DE/translation.json index bc1bd1f2..1763e428 100644 --- a/apps/client/public/locales/de-DE/translation.json +++ b/apps/client/public/locales/de-DE/translation.json @@ -42,7 +42,7 @@ "Delete group": "Gruppe löschen", "Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Sind Sie sicher, dass Sie diese Seite löschen möchten? Dadurch werden ihre Unterseiten und die Seitengeschichte gelöscht. Diese Aktion ist unwiderruflich.", "Description": "Beschreibung", - "Details": "Einzelheiten", + "Details": "Details", "e.g ACME": "z.B. ACME", "e.g ACME Inc": "z.B. ACME Inc.", "e.g Developers": "z.B. Entwickler", @@ -234,9 +234,7 @@ "Anyone with this link can join this workspace.": "Jeder mit diesem Link kann dem Arbeitsbereich beitreten.", "Invite link": "Einladungslink", "Copy": "Kopieren", - "Copy to space": "In Raum kopieren", "Copied": "Kopiert", - "Duplicate": "Duplizieren", "Select a user": "Benutzer auswählen", "Select a group": "Gruppe auswählen", "Export all pages and attachments in this space.": "Alle Seiten und Anhänge in diesem Bereich exportieren.", @@ -527,5 +525,47 @@ "Delete SSO provider": "SSO-Anbieter löschen", "Are you sure you want to delete this SSO provider?": "Sind Sie sicher, dass Sie diesen SSO-Anbieter löschen möchten?", "Action": "Aktion", - "{{ssoProviderType}} configuration": "{{ssoProviderType}}-Konfiguration" + "{{ssoProviderType}} configuration": "{{ssoProviderType}}-Konfiguration", + "Icon": "Icon", + "Upload image": "Bild hochladen", + "Remove image": "Bild entfernen", + "Failed to remove image": "Fehler beim Entfernen des Bildes", + "Image exceeds 10MB limit.": "Bild überschreitet das Limit von 10 MB.", + "Image removed successfully": "Bild erfolgreich entfernt", + "API key": "API-Schlüssel", + "API key created successfully": "API-Schlüssel erfolgreich erstellt", + "API keys": "API-Schlüssel", + "API management": "API-Verwaltung", + "Are you sure you want to revoke this API key": "Sind Sie sicher, dass Sie diesen API-Schlüssel widerrufen möchten?", + "Create API Key": "API-Schlüssel erstellen", + "Custom expiration date": "Benutzerdefiniertes Ablaufdatum", + "Enter a descriptive token name": "Geben Sie einen beschreibenden Token-Namen ein", + "Expiration": "Ablauf", + "Expired": "Abgelaufen", + "Expires": "Läuft ab", + "I've saved my API key": "Ich habe meinen API-Schlüssel gespeichert", + "Last use": "Zuletzt verwendet", + "No API keys found": "Keine API-Schlüssel gefunden", + "No expiration": "Kein Ablauf", + "Revoke API key": "API-Schlüssel widerrufen", + "Revoked successfully": "Erfolgreich widerrufen", + "Select expiration date": "Ablaufdatum wählen", + "This action cannot be undone. Any applications using this API key will stop working.": "Diese Aktion kann nicht rückgängig gemacht werden. Alle Anwendungen, die diesen API-Schlüssel verwenden, werden nicht mehr funktionieren.", + "Update API key": "API-Schlüssel aktualisieren", + "Manage API keys for all users in the workspace": "Verwalten Sie API-Schlüssel für alle Benutzer im Arbeitsbereich", + "AI settings": "KI-Einstellungen", + "AI search": "KI-Suche", + "AI Answer": "KI-Antwort", + "Ask AI": "KI fragen", + "AI is thinking...": "Die KI überlegt...", + "Ask a question...": "Fragen stellen...", + "AI-powered search (Ask AI)": "KI-gestützte Suche (KI fragen)", + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "Die KI-Suche verwendet Vektor-Einbettungen, um semantische Suchfunktionen in Ihrem Arbeitsbereich bereitzustellen.", + "Toggle AI search": "KI-Suche umschalten", + "Sources": "Quellen", + "Ask AI not available for attachments": "KI fragen nicht für Anhänge verfügbar", + "No answer available": "Keine Antwort verfügbar", + "Background color": "Hintergrundfarbe", + "Highlight color": "Hervorhebungsfarbe", + "Remove color": "Farbe entfernen" } diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index 6d9e548b..8cb33378 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -533,5 +533,41 @@ "Remove image": "Remove image", "Failed to remove image": "Failed to remove image", "Image exceeds 10MB limit.": "Image exceeds 10MB limit.", - "Image removed successfully": "Image removed successfully" + "Image removed successfully": "Image removed successfully", + "API key": "API key", + "API key created successfully": "API key created successfully", + "API keys": "API keys", + "API management": "API management", + "Are you sure you want to revoke this API key": "Are you sure you want to revoke this API key", + "Create API Key": "Create API Key", + "Custom expiration date": "Custom expiration date", + "Enter a descriptive token name": "Enter a descriptive token name", + "Expiration": "Expiration", + "Expired": "Expired", + "Expires": "Expires", + "I've saved my API key": "I've saved my API key", + "Last use": "Last Used", + "No API keys found": "No API keys found", + "No expiration": "No expiration", + "Revoke API key": "Revoke API key", + "Revoked successfully": "Revoked successfully", + "Select expiration date": "Select expiration date", + "This action cannot be undone. Any applications using this API key will stop working.": "This action cannot be undone. Any applications using this API key will stop working.", + "Update API key": "Update API key", + "Manage API keys for all users in the workspace": "Manage API keys for all users in the workspace", + "AI settings": "AI settings", + "AI search": "AI search", + "AI Answer": "AI Answer", + "Ask AI": "Ask AI", + "AI is thinking...": "AI is thinking...", + "Ask a question...": "Ask a question...", + "AI-powered search (Ask AI)": "AI-powered search (Ask AI)", + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.", + "Toggle AI search": "Toggle AI search", + "Sources": "Sources", + "Ask AI not available for attachments": "Ask AI not available for attachments", + "No answer available": "No answer available", + "Background color": "Background color", + "Highlight color": "Highlight color", + "Remove color": "Remove color" } diff --git a/apps/client/public/locales/es-ES/translation.json b/apps/client/public/locales/es-ES/translation.json index 3450f27d..f99e8541 100644 --- a/apps/client/public/locales/es-ES/translation.json +++ b/apps/client/public/locales/es-ES/translation.json @@ -527,5 +527,47 @@ "Delete SSO provider": "Eliminar proveedor de SSO", "Are you sure you want to delete this SSO provider?": "¿Está seguro de que desea eliminar este proveedor de SSO?", "Action": "Acción", - "{{ssoProviderType}} configuration": "Configuración de {{ssoProviderType}}" + "{{ssoProviderType}} configuration": "Configuración de {{ssoProviderType}}", + "Icon": "Icono", + "Upload image": "Subir imagen", + "Remove image": "Eliminar imagen", + "Failed to remove image": "No se ha podido eliminar la imagen", + "Image exceeds 10MB limit.": "La imagen excede del límite de 10 MB", + "Image removed successfully": "Imagen eliminada correctamente", + "API key": "Clave API", + "API key created successfully": "Clave API creada correctamente", + "API keys": "Claves API", + "API management": "Gestión de API", + "Are you sure you want to revoke this API key": "¿Está seguro de que desea revocar esta clave API? ", + "Create API Key": "Crear clave API", + "Custom expiration date": "Fecha de vencimiento personalizada", + "Enter a descriptive token name": "Introduce un nombre descriptivo del token", + "Expiration": "Vencimiento", + "Expired": "Vencido", + "Expires": "Vence", + "I've saved my API key": "He guardado mi clave API", + "Last use": "Último uso", + "No API keys found": "No se han encontrado claves API", + "No expiration": "Sin vencimiento", + "Revoke API key": "Revocar clave API", + "Revoked successfully": "Revocada correctamente", + "Select expiration date": "Seleccionar fecha de vencimiento", + "This action cannot be undone. Any applications using this API key will stop working.": "Esta acción no se puede deshacer. Las aplicaciones que utilicen esta clave API dejarán de funcionar.", + "Update API key": "Actualizar clave API", + "Manage API keys for all users in the workspace": "Gestionar claves API para todos los usuarios en el espacio de trabajo", + "AI settings": "Configuración de IA", + "AI search": "Búsqueda de IA", + "AI Answer": "Respuesta de IA", + "Ask AI": "Preguntar a IA", + "AI is thinking...": "IA está pensando...", + "Ask a question...": "Haz una pregunta...", + "AI-powered search (Ask AI)": "Búsqueda impulsada por IA (Preguntar a IA)", + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "La búsqueda de IA utiliza incrustaciones vectoriales para proporcionar capacidades de búsqueda semántica en todo el contenido de su espacio de trabajo.", + "Toggle AI search": "Alternar búsqueda de IA", + "Sources": "Fuentes", + "Ask AI not available for attachments": "Preguntar a IA no está disponible para adjuntos", + "No answer available": "No hay respuesta disponible", + "Background color": "Color de fondo", + "Highlight color": "Color de resaltado", + "Remove color": "Eliminar color" } diff --git a/apps/client/public/locales/fr-FR/translation.json b/apps/client/public/locales/fr-FR/translation.json index 0dbd62ac..5644d719 100644 --- a/apps/client/public/locales/fr-FR/translation.json +++ b/apps/client/public/locales/fr-FR/translation.json @@ -527,5 +527,47 @@ "Delete SSO provider": "Supprimer le fournisseur SSO", "Are you sure you want to delete this SSO provider?": "Êtes-vous sûr de vouloir supprimer ce fournisseur SSO ?", "Action": "Action", - "{{ssoProviderType}} configuration": "Configuration {{ssoProviderType}}" + "{{ssoProviderType}} configuration": "Configuration {{ssoProviderType}}", + "Icon": "Icône", + "Upload image": "Téléverser une image", + "Remove image": "Supprimer l'image", + "Failed to remove image": "Échec de la suppression de l'image", + "Image exceeds 10MB limit.": "L'image dépasse la limite de 10 Mo.", + "Image removed successfully": "Image supprimée avec succès", + "API key": "Clé API", + "API key created successfully": "Clé API créée avec succès", + "API keys": "Clés API", + "API management": "Gestion des API", + "Are you sure you want to revoke this API key": "Êtes-vous sûr de vouloir révoquer cette clé API", + "Create API Key": "Créer une clé API", + "Custom expiration date": "Date d'expiration personnalisée", + "Enter a descriptive token name": "Entrez un nom descriptif pour le jeton", + "Expiration": "Expiration", + "Expired": "Expiré(e)", + "Expires": "Expire", + "I've saved my API key": "J'ai enregistré ma clé API", + "Last use": "Dernière utilisation", + "No API keys found": "Aucune clé API trouvée", + "No expiration": "Pas d'expiration", + "Revoke API key": "Révoquer la clé API", + "Revoked successfully": "Révoqué(e) avec succès", + "Select expiration date": "Sélectionnez la date d'expiration", + "This action cannot be undone. Any applications using this API key will stop working.": "Cette action ne peut pas être annulée. Toutes les applications utilisant cette clé API cesseront de fonctionner.", + "Update API key": "Mettre à jour la clé API", + "Manage API keys for all users in the workspace": "Gérer les clés API pour tous les utilisateurs dans l'espace de travail", + "AI settings": "Paramètres de l'IA", + "AI search": "Recherche IA", + "AI Answer": "Réponse IA", + "Ask AI": "Demander à l'IA", + "AI is thinking...": "L'IA réfléchit...", + "Ask a question...": "Posez une question...", + "AI-powered search (Ask AI)": "Recherche assistée par l'IA (Demander à l'IA)", + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "La recherche IA utilise des incorporations vectorielles pour fournir des capacités de recherche sémantique à travers le contenu de votre espace de travail.", + "Toggle AI search": "Basculer la recherche IA", + "Sources": "Sources", + "Ask AI not available for attachments": "Demande à l'IA non disponible pour les pièces jointes", + "No answer available": "Pas de réponse disponible", + "Background color": "Couleur de fond", + "Highlight color": "Couleur de surbrillance", + "Remove color": "Supprimer la couleur" } diff --git a/apps/client/public/locales/it-IT/translation.json b/apps/client/public/locales/it-IT/translation.json index 8ed1f2c8..8d00f451 100644 --- a/apps/client/public/locales/it-IT/translation.json +++ b/apps/client/public/locales/it-IT/translation.json @@ -527,5 +527,47 @@ "Delete SSO provider": "Elimina provider SSO", "Are you sure you want to delete this SSO provider?": "Sei sicuro di voler eliminare questo provider SSO?", "Action": "Azione", - "{{ssoProviderType}} configuration": "Configurazione {{ssoProviderType}}" + "{{ssoProviderType}} configuration": "Configurazione {{ssoProviderType}}", + "Icon": "Icona", + "Upload image": "Carica immagine", + "Remove image": "Rimuovi immagine", + "Failed to remove image": "Rimozione immagine fallita", + "Image exceeds 10MB limit.": "L'immagine supera il limite di 10MB.", + "Image removed successfully": "Immagine rimossa con successo", + "API key": "Chiave API", + "API key created successfully": "Chiave API creata con successo", + "API keys": "Chiavi API", + "API management": "Gestione API", + "Are you sure you want to revoke this API key": "Sei sicuro di voler revocare questa chiave API", + "Create API Key": "Crea Chiave API", + "Custom expiration date": "Data di scadenza personalizzata", + "Enter a descriptive token name": "Inserisci un nome descrittivo del token", + "Expiration": "Scadenza", + "Expired": "Scaduto", + "Expires": "Scade", + "I've saved my API key": "Ho salvato la mia chiave API", + "Last use": "Ultimo utilizzo", + "No API keys found": "Nessuna chiave API trovata", + "No expiration": "Nessuna scadenza", + "Revoke API key": "Revoca chiave API", + "Revoked successfully": "Revocata con successo", + "Select expiration date": "Seleziona la data di scadenza", + "This action cannot be undone. Any applications using this API key will stop working.": "Questa azione non può essere annullata. Qualsiasi applicazione che utilizza questa chiave API smetterà di funzionare.", + "Update API key": "Aggiorna chiave API", + "Manage API keys for all users in the workspace": "Gestisci le chiavi API per tutti gli utenti nell'area di lavoro", + "AI settings": "Impostazioni AI", + "AI search": "Ricerca AI", + "AI Answer": "Risposta AI", + "Ask AI": "Chiedi all'AI", + "AI is thinking...": "L'AI sta pensando...", + "Ask a question...": "Fai una domanda...", + "AI-powered search (Ask AI)": "Ricerca potenziata dall'AI (Chiedi all'AI)", + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "La ricerca AI utilizza embeddings vettoriali per fornire capacità di ricerca semantica nel contenuto della tua area di lavoro.", + "Toggle AI search": "Attiva/disattiva ricerca AI", + "Sources": "Fonti", + "Ask AI not available for attachments": "Chiedere all'AI non è disponibile per gli allegati", + "No answer available": "Nessuna risposta disponibile", + "Background color": "Colore di sfondo", + "Highlight color": "Colore evidenziato", + "Remove color": "Rimuovi colore" } diff --git a/apps/client/public/locales/ja-JP/translation.json b/apps/client/public/locales/ja-JP/translation.json index 4e1811f3..6ea006d7 100644 --- a/apps/client/public/locales/ja-JP/translation.json +++ b/apps/client/public/locales/ja-JP/translation.json @@ -527,5 +527,47 @@ "Delete SSO provider": "SSOプロバイダーを削除する", "Are you sure you want to delete this SSO provider?": "このSSOプロバイダーを削除してもよろしいですか?", "Action": "アクション", - "{{ssoProviderType}} configuration": "{{ssoProviderType}}の構成" + "{{ssoProviderType}} configuration": "{{ssoProviderType}}の構成", + "Icon": "アイコン", + "Upload image": "画像をアップロード", + "Remove image": "画像を削除", + "Failed to remove image": "画像の削除に失敗しました", + "Image exceeds 10MB limit.": "画像が10MBの制限を超えています。", + "Image removed successfully": "画像が正常に削除されました", + "API key": "APIキー", + "API key created successfully": "APIキーが正常に作成されました", + "API keys": "APIキー", + "API management": "API管理", + "Are you sure you want to revoke this API key": "このAPIキーを無効にしてもよろしいですか", + "Create API Key": "APIキーを作成", + "Custom expiration date": "カスタム有効期限", + "Enter a descriptive token name": "説明的なトークン名を入力してください", + "Expiration": "有効期限", + "Expired": "期限切れ", + "Expires": "期限が切れます", + "I've saved my API key": "APIキーを保存しました", + "Last use": "最終使用", + "No API keys found": "APIキーが見つかりません", + "No expiration": "期限なし", + "Revoke API key": "APIキーを無効にする", + "Revoked successfully": "正常に無効化されました", + "Select expiration date": "有効期限を選択してください", + "This action cannot be undone. Any applications using this API key will stop working.": "この操作は元に戻せません。このAPIキーを使用しているアプリケーションは動作を停止します。", + "Update API key": "APIキーを更新", + "Manage API keys for all users in the workspace": "ワークスペース内のすべてのユーザーのAPIキーを管理", + "AI settings": "AI設定", + "AI search": "AI検索", + "AI Answer": "AI回答", + "Ask AI": "AIに質問する", + "AI is thinking...": "AIが考え中...", + "Ask a question...": "質問を入力...", + "AI-powered search (Ask AI)": "AIによる検索(AIに質問)", + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "AI検索はベクター埋め込みを使用して、ワークスペースコンテンツ全体にわたって意味検索機能を提供します。", + "Toggle AI search": "AI検索を切り替え", + "Sources": "ソース", + "Ask AI not available for attachments": "添付ファイルにはAI質問は利用できません", + "No answer available": "回答がありません", + "Background color": "背景色", + "Highlight color": "ハイライト色", + "Remove color": "色を削除" } diff --git a/apps/client/public/locales/ko-KR/translation.json b/apps/client/public/locales/ko-KR/translation.json index c6e3dc88..6e1f5b24 100644 --- a/apps/client/public/locales/ko-KR/translation.json +++ b/apps/client/public/locales/ko-KR/translation.json @@ -527,5 +527,47 @@ "Delete SSO provider": "SSO 제공자 삭제", "Are you sure you want to delete this SSO provider?": "이 SSO 제공자를 삭제하시겠습니까?", "Action": "작업", - "{{ssoProviderType}} configuration": "{{ssoProviderType}} 구성" + "{{ssoProviderType}} configuration": "{{ssoProviderType}} 구성", + "Icon": "아이콘", + "Upload image": "이미지 업로드", + "Remove image": "이미지 제거", + "Failed to remove image": "이미지 제거 실패", + "Image exceeds 10MB limit.": "이미지가 10MB 용량 제한을 초과합니다.", + "Image removed successfully": "이미지가 성공적으로 제거되었습니다", + "API key": "API 키", + "API key created successfully": "API 키 생성 완료", + "API keys": "API 키", + "API management": "API 관리", + "Are you sure you want to revoke this API key": "이 API 키를 취소하시겠습니까?", + "Create API Key": "API 키 생성", + "Custom expiration date": "사용자 정의 만료일", + "Enter a descriptive token name": "토큰 이름을 입력하세요", + "Expiration": "만료", + "Expired": "만료됨", + "Expires": "만료일", + "I've saved my API key": "API 키를 저장했습니다", + "Last use": "최근 사용", + "No API keys found": "API 키를 찾을 수 없습니다", + "No expiration": "유효기간 없음", + "Revoke API key": "API 키 취소", + "Revoked successfully": "성공적으로 취소되었습니다", + "Select expiration date": "만료일 선택", + "This action cannot be undone. Any applications using this API key will stop working.": "이 작업은 되돌릴 수 없습니다. 이 API 키를 사용하는 모든 응용 프로그램이 작동을 멈출 것입니다.", + "Update API key": "API 키 갱신", + "Manage API keys for all users in the workspace": "워크스페이스 내 모든 사용자의 API 키 관리", + "AI settings": "AI 설정", + "AI search": "AI 검색", + "AI Answer": "AI 답변", + "Ask AI": "AI에게 묻기", + "AI is thinking...": "AI가 생각 중입니다...", + "Ask a question...": "질문하세요...", + "AI-powered search (Ask AI)": "AI 지원 검색 (AI에게 묻기)", + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "AI 검색은 벡터 임베딩을 사용하여 작업공간 콘텐츠에 대한 의미 검색 기능을 제공합니다.", + "Toggle AI search": "AI 검색 전환", + "Sources": "출처", + "Ask AI not available for attachments": "AI에게 묻기 기능은 첨부 파일에 대해 사용할 수 없습니다", + "No answer available": "답변을 제공할 수 없습니다", + "Background color": "배경 색", + "Highlight color": "강조 색", + "Remove color": "색 제거" } diff --git a/apps/client/public/locales/nl-NL/translation.json b/apps/client/public/locales/nl-NL/translation.json index 7429bfe1..7db6836d 100644 --- a/apps/client/public/locales/nl-NL/translation.json +++ b/apps/client/public/locales/nl-NL/translation.json @@ -34,7 +34,7 @@ "Create group": "Groep aanmaken", "Create page": "Pagina aanmaken", "Create space": "Ruimte aanmaken", - "Create workspace": "Wwerkruimte aanmaken", + "Create workspace": "Werkruimte aanmaken", "Current password": "Huidig wachtwoord", "Dark": "Donker", "Date": "Datum", @@ -91,7 +91,7 @@ "Invite by email": "Uitnodigen via e-mail", "Invite members": "Leden uitnodigen", "Invite new members": "Nieuwe leden uitnodigen", - "Invited members who are yet to accept their invitation will appear here.": "Uigenodigde leden die hun uitnodiging nog moeten accepteren zullen hier worden getoond.", + "Invited members who are yet to accept their invitation will appear here.": "Uitgenodigde leden die hun uitnodiging nog moeten accepteren zullen hier worden getoond.", "Invited members will be granted access to spaces the groups can access": "Uitgenodigde leden wordt toegang gegeven tot ruimtes de groepen toegang toe heeft", "Join the workspace": "Word lid van de werkruimte", "Language": "Taal", @@ -527,5 +527,47 @@ "Delete SSO provider": "Verwijder SSO-provider", "Are you sure you want to delete this SSO provider?": "Weet u zeker dat u deze SSO-provider wilt verwijderen?", "Action": "Actie", - "{{ssoProviderType}} configuration": "{{ssoProviderType}} configuratie" + "{{ssoProviderType}} configuration": "{{ssoProviderType}} configuratie", + "Icon": "Icoon", + "Upload image": "Afbeelding uploaden", + "Remove image": "Afbeelding verwijderen", + "Failed to remove image": "Afbeelding verwijderen mislukt", + "Image exceeds 10MB limit.": "Afbeelding overschrijdt de limiet van 10MB.", + "Image removed successfully": "Afbeelding succesvol verwijderd", + "API key": "API-sleutel", + "API key created successfully": "API-sleutel succesvol aangemaakt", + "API keys": "API-sleutels", + "API management": "API-beheer", + "Are you sure you want to revoke this API key": "Weet u zeker dat u deze API-sleutel wilt intrekken", + "Create API Key": "API-sleutel aanmaken", + "Custom expiration date": "Aangepaste vervaldatum", + "Enter a descriptive token name": "Voer een beschrijvende tokennaam in", + "Expiration": "Vervaldatum", + "Expired": "Verlopen", + "Expires": "Verloopt", + "I've saved my API key": "Ik heb mijn API-sleutel opgeslagen", + "Last use": "Laatst gebruikt", + "No API keys found": "Geen API-sleutels gevonden", + "No expiration": "Geen vervaldatum", + "Revoke API key": "API-sleutel intrekken", + "Revoked successfully": "Succesvol ingetrokken", + "Select expiration date": "Selecteer vervaldatum", + "This action cannot be undone. Any applications using this API key will stop working.": "Deze actie kan niet ongedaan worden gemaakt. Alle toepassingen die deze API-sleutel gebruiken, zullen niet meer werken.", + "Update API key": "API-sleutel bijwerken", + "Manage API keys for all users in the workspace": "Beheer API-sleutels voor alle gebruikers in de werkruimte", + "AI settings": "AI-instellingen", + "AI search": "AI-zoekopdracht", + "AI Answer": "AI Antwoord", + "Ask AI": "Vraag AI", + "AI is thinking...": "AI is aan het nadenken...", + "Ask a question...": "Stel een vraag...", + "AI-powered search (Ask AI)": "AI-ondersteunde zoekopdracht (Vraag AI)", + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "AI-zoekopdracht maakt gebruik van vectorembeddings om semantische zoekmogelijkheden te bieden in uw werkruimte-inhoud.", + "Toggle AI search": "Schakel AI-zoekopdracht in/uit", + "Sources": "Bronnen", + "Ask AI not available for attachments": "Vraag AI is niet beschikbaar voor bijlages", + "No answer available": "Geen antwoord beschikbaar", + "Background color": "Achtergrondkleur", + "Highlight color": "Markeerkleur", + "Remove color": "Kleur verwijderen" } diff --git a/apps/client/public/locales/pt-BR/translation.json b/apps/client/public/locales/pt-BR/translation.json index c4564830..5d11ec7a 100644 --- a/apps/client/public/locales/pt-BR/translation.json +++ b/apps/client/public/locales/pt-BR/translation.json @@ -527,5 +527,47 @@ "Delete SSO provider": "Excluir provedor de SSO", "Are you sure you want to delete this SSO provider?": "Tem certeza de que deseja excluir este provedor de SSO?", "Action": "Ação", - "{{ssoProviderType}} configuration": "Configuração de {{ssoProviderType}}" + "{{ssoProviderType}} configuration": "Configuração de {{ssoProviderType}}", + "Icon": "Ícone", + "Upload image": "Fazer upload da imagem", + "Remove image": "Remover imagem", + "Failed to remove image": "Falha ao remover imagem", + "Image exceeds 10MB limit.": "A imagem excede o limite de 10MB.", + "Image removed successfully": "Imagem removida com sucesso", + "API key": "Chave API", + "API key created successfully": "Chave API criada com sucesso", + "API keys": "Chaves API", + "API management": "Gestão de API", + "Are you sure you want to revoke this API key": "Tem certeza de que deseja revogar esta chave API", + "Create API Key": "Criar Chave API", + "Custom expiration date": "Data de expiração personalizada", + "Enter a descriptive token name": "Insira um nome descritivo para o token", + "Expiration": "Expiração", + "Expired": "Expirado", + "Expires": "Expira", + "I've saved my API key": "Salvei minha chave API", + "Last use": "Último uso", + "No API keys found": "Nenhuma chave API encontrada", + "No expiration": "Sem expiração", + "Revoke API key": "Revogar chave API", + "Revoked successfully": "Revogada com sucesso", + "Select expiration date": "Selecionar data de expiração", + "This action cannot be undone. Any applications using this API key will stop working.": "Esta ação não pode ser desfeita. Qualquer aplicação usando esta chave API deixará de funcionar.", + "Update API key": "Atualizar chave API", + "Manage API keys for all users in the workspace": "Gerenciar chaves API para todos os usuários no espaço de trabalho", + "AI settings": "Configurações de IA", + "AI search": "Pesquisa IA", + "AI Answer": "Resposta de IA", + "Ask AI": "Pergunte à IA", + "AI is thinking...": "IA está pensando...", + "Ask a question...": "Faça uma pergunta...", + "AI-powered search (Ask AI)": "Pesquisa com IA (Pergunte à IA)", + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "A pesquisa IA usa vetores de incorporação para fornecer capacidades de pesquisa semântica em todo o conteúdo do seu espaço de trabalho.", + "Toggle AI search": "Alternar pesquisa de IA", + "Sources": "Fontes", + "Ask AI not available for attachments": "Perguntar à IA não está disponível para anexos", + "No answer available": "Nenhuma resposta disponível", + "Background color": "Cor de fundo", + "Highlight color": "Cor de destaque", + "Remove color": "Remover cor" } diff --git a/apps/client/public/locales/ru-RU/translation.json b/apps/client/public/locales/ru-RU/translation.json index fab5389e..f1a9cd85 100644 --- a/apps/client/public/locales/ru-RU/translation.json +++ b/apps/client/public/locales/ru-RU/translation.json @@ -498,10 +498,10 @@ "Deleted at": "Удалено в", "Preview": "Предпросмотр", "Subpages": "Подстраницы", - "Failed to load subpages": "Не удалось загрузить подстраницы", + "Failed to load subpages": "Не удалось загрузить под страницы", "No subpages": "Нет подстраниц", "Subpages (Child pages)": "Подстраницы (вложенные страницы)", - "List all subpages of the current page": "Показать все подстраницы текущей страницы", + "List all subpages of the current page": "Показать все под страницы", "Attachments": "Вложения", "All spaces": "Все пространства", "Unknown": "Неизвестно", @@ -527,5 +527,47 @@ "Delete SSO provider": "Удалить поставщика SSO", "Are you sure you want to delete this SSO provider?": "Вы уверены, что хотите удалить этого поставщика SSO?", "Action": "Действие", - "{{ssoProviderType}} configuration": "Настройка {{ssoProviderType}}" + "{{ssoProviderType}} configuration": "Настройка {{ssoProviderType}}", + "Icon": "Иконка", + "Upload image": "Загрузить изображение", + "Remove image": "Удалить изображение", + "Failed to remove image": "Не удалось удалить изображение", + "Image exceeds 10MB limit.": "Изображение превышает предел 10MB.", + "Image removed successfully": "Изображение успешно удалено", + "API key": "API ключ", + "API key created successfully": "API ключ успешно создан", + "API keys": "API ключи", + "API management": "Управление API", + "Are you sure you want to revoke this API key": "Вы уверены, что хотите отозвать этот API ключ", + "Create API Key": "Создать API ключ", + "Custom expiration date": "Пользовательская дата срока действия", + "Enter a descriptive token name": "Введите понятное имя токена", + "Expiration": "Срок действия", + "Expired": "Истек", + "Expires": "Истекает", + "I've saved my API key": "Я сохранил мой API ключ", + "Last use": "Последнее использование", + "No API keys found": "API ключи не найдены", + "No expiration": "Не истекает", + "Revoke API key": "Отозвать API ключ", + "Revoked successfully": "Отозван успешно", + "Select expiration date": "Выберете срок действия", + "This action cannot be undone. Any applications using this API key will stop working.": "Это действие необратимо. Любые приложения, использующие этот API ключ, перестанут работать.", + "Update API key": "Обновить API ключ", + "Manage API keys for all users in the workspace": "Управлять API ключами для всех пользователей в рабочей области", + "AI settings": "Настройки ИИ", + "AI search": "Поиск ИИ", + "AI Answer": "Ответ ИИ", + "Ask AI": "Спросить ИИ", + "AI is thinking...": "ИИ обрабатывает запрос...", + "Ask a question...": "Задайте вопрос...", + "AI-powered search (Ask AI)": "Поиск на базе ИИ (Спросить ИИ)", + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "Поиск ИИ использует векторные встраивания для обеспечения семантического поиска по содержимому вашего рабочего пространства.", + "Toggle AI search": "Переключить поиск ИИ", + "Sources": "Источники", + "Ask AI not available for attachments": "Функция \"Спросить ИИ\" недоступна для вложений", + "No answer available": "Ответ недоступен", + "Background color": "Цвет фона", + "Highlight color": "Цвет выделения", + "Remove color": "Удалить цвет" } diff --git a/apps/client/public/locales/uk-UA/translation.json b/apps/client/public/locales/uk-UA/translation.json index e6d5427f..2fb44ad1 100644 --- a/apps/client/public/locales/uk-UA/translation.json +++ b/apps/client/public/locales/uk-UA/translation.json @@ -527,5 +527,47 @@ "Delete SSO provider": "Видалити постачальника SSO", "Are you sure you want to delete this SSO provider?": "Ви впевнені, що хочете видалити цього постачальника SSO?", "Action": "Дія", - "{{ssoProviderType}} configuration": "Конфігурація {{ssoProviderType}}" + "{{ssoProviderType}} configuration": "Конфігурація {{ssoProviderType}}", + "Icon": "Іконка", + "Upload image": "Завантажити зображення", + "Remove image": "Видалити зображення", + "Failed to remove image": "Не вдалося видалити зображення", + "Image exceeds 10MB limit.": "Зображення має займати менше, ніж 10 МБ.", + "Image removed successfully": "Зображення видалено", + "API key": "Ключ API", + "API key created successfully": "Ключ API успішно створено", + "API keys": "Ключі API", + "API management": "Управління API", + "Are you sure you want to revoke this API key": "Ви впевнені, що хочете відкликати цей ключ API", + "Create API Key": "Створити ключ API", + "Custom expiration date": "Користувацька дата закінчення", + "Enter a descriptive token name": "Введіть описову назву токена", + "Expiration": "Термін дії", + "Expired": "Закінчився", + "Expires": "Закінчується", + "I've saved my API key": "Я зберіг свій ключ API", + "Last use": "Останнє використання", + "No API keys found": "Ключі API не знайдено", + "No expiration": "Без терміну дії", + "Revoke API key": "Відкликати ключ API", + "Revoked successfully": "Успішно відкликано", + "Select expiration date": "Виберіть дату закінчення", + "This action cannot be undone. Any applications using this API key will stop working.": "Цю дію не можна скасувати. Будь-які додатки, що використовують цей ключ API, перестануть працювати.", + "Update API key": "Оновити ключ API", + "Manage API keys for all users in the workspace": "Керувати ключами API для всіх користувачів у робочій області", + "AI settings": "Налаштування ШІ", + "AI search": "Пошук з ШІ", + "AI Answer": "Відповідь ШІ", + "Ask AI": "Запитати ШІ", + "AI is thinking...": "ШІ думає...", + "Ask a question...": "Задайте питання...", + "AI-powered search (Ask AI)": "Пошук на базі ШІ (Запитати ШІ)", + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "Пошук з ШІ використовує векторні вбудовування для надання можливостей семантичного пошуку у вашому робочому вмісті.", + "Toggle AI search": "Переключити пошук з ШІ", + "Sources": "Джерела", + "Ask AI not available for attachments": "Запитати ШІ недоступно для вкладень", + "No answer available": "Відповідь недоступна", + "Background color": "Колір фону", + "Highlight color": "Колір підсвічування", + "Remove color": "Видалити колір" } diff --git a/apps/client/public/locales/zh-CN/translation.json b/apps/client/public/locales/zh-CN/translation.json index 33373155..d4b25deb 100644 --- a/apps/client/public/locales/zh-CN/translation.json +++ b/apps/client/public/locales/zh-CN/translation.json @@ -527,5 +527,47 @@ "Delete SSO provider": "删除SSO提供商", "Are you sure you want to delete this SSO provider?": "您确定要删除此SSO提供商吗?", "Action": "操作", - "{{ssoProviderType}} configuration": "{{ssoProviderType}} 配置" + "{{ssoProviderType}} configuration": "{{ssoProviderType}} 配置", + "Icon": "图标", + "Upload image": "上传图片", + "Remove image": "删除图片", + "Failed to remove image": "无法删除图片", + "Image exceeds 10MB limit.": "图片超过10MB限制。", + "Image removed successfully": "图片删除成功", + "API key": "API密钥", + "API key created successfully": "API密钥创建成功", + "API keys": "API密钥", + "API management": "API管理", + "Are you sure you want to revoke this API key": "确定要撤销此API密钥吗", + "Create API Key": "创建API密钥", + "Custom expiration date": "自定义到期日期", + "Enter a descriptive token name": "输入描述性令牌名称", + "Expiration": "到期", + "Expired": "已过期", + "Expires": "到期", + "I've saved my API key": "我已保存我的API密钥", + "Last use": "上次使用", + "No API keys found": "找不到API密钥", + "No expiration": "无到期", + "Revoke API key": "撤销API密钥", + "Revoked successfully": "撤销成功", + "Select expiration date": "选择到期日期", + "This action cannot be undone. Any applications using this API key will stop working.": "此操作无法撤销。使用此API密钥的任何应用程序将停止工作。", + "Update API key": "更新API密钥", + "Manage API keys for all users in the workspace": "管理工作空间中所有用户的API密钥", + "AI settings": "AI设置", + "AI search": "AI搜索", + "AI Answer": "AI回答", + "Ask AI": "询问AI", + "AI is thinking...": "AI正在思考...", + "Ask a question...": "提问...", + "AI-powered search (Ask AI)": "AI驱动的搜索(询问AI)", + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.": "AI搜索使用向量嵌入提供跨工作空间内容的语义搜索功能。", + "Toggle AI search": "切换AI搜索", + "Sources": "来源", + "Ask AI not available for attachments": "附件不支持询问AI", + "No answer available": "无可用答案", + "Background color": "背景颜色", + "Highlight color": "突出显示颜色", + "Remove color": "移除颜色" } diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index 3995191d..e0df67a7 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -35,6 +35,9 @@ import SpacesPage from "@/pages/spaces/spaces.tsx"; import { MfaChallengePage } from "@/ee/mfa/pages/mfa-challenge-page"; import { MfaSetupRequiredPage } from "@/ee/mfa/pages/mfa-setup-required-page"; import SpaceTrash from "@/pages/space/space-trash.tsx"; +import UserApiKeys from "@/ee/api-key/pages/user-api-keys"; +import WorkspaceApiKeys from "@/ee/api-key/pages/workspace-api-keys"; +import AiSettings from "@/ee/ai/pages/ai-settings.tsx"; export default function App() { const { t } = useTranslation(); @@ -96,13 +99,16 @@ export default function App() { path={"account/preferences"} element={} /> + } /> } /> } /> + } /> } /> } /> } /> } /> } /> + } /> {!isCloud() && } />} {isCloud() && } />} diff --git a/apps/client/src/components/common/no-table-results.tsx b/apps/client/src/components/common/no-table-results.tsx index 124bbb9b..0f34fa2f 100644 --- a/apps/client/src/components/common/no-table-results.tsx +++ b/apps/client/src/components/common/no-table-results.tsx @@ -4,14 +4,15 @@ import { useTranslation } from "react-i18next"; interface NoTableResultsProps { colSpan: number; + text?: string; } -export default function NoTableResults({ colSpan }: NoTableResultsProps) { +export default function NoTableResults({ colSpan, text }: NoTableResultsProps) { const { t } = useTranslation(); return ( - {t("No results found...")} + {text || t("No results found...")} diff --git a/apps/client/src/components/settings/settings-queries.tsx b/apps/client/src/components/settings/settings-queries.tsx index 2f3b46bd..bc528466 100644 --- a/apps/client/src/components/settings/settings-queries.tsx +++ b/apps/client/src/components/settings/settings-queries.tsx @@ -10,6 +10,7 @@ import { getWorkspaceMembers } from "@/features/workspace/services/workspace-ser import { getLicenseInfo } from "@/ee/licence/services/license-service.ts"; import { getSsoProviders } from "@/ee/security/services/security-service.ts"; import { getShares } from "@/features/share/services/share-service.ts"; +import { getApiKeys } from "@/ee/api-key"; export const prefetchWorkspaceMembers = () => { const params = { limit: 100, page: 1, query: "" } as QueryParams; @@ -65,3 +66,17 @@ export const prefetchShares = () => { queryFn: () => getShares({ page: 1, limit: 100 }), }); }; + +export const prefetchApiKeys = () => { + queryClient.prefetchQuery({ + queryKey: ["api-key-list", { page: 1 }], + queryFn: () => getApiKeys({ page: 1 }), + }); +}; + +export const prefetchApiKeyManagement = () => { + queryClient.prefetchQuery({ + queryKey: ["api-key-list", { page: 1 }], + queryFn: () => getApiKeys({ page: 1, adminView: true }), + }); +}; diff --git a/apps/client/src/components/settings/settings-sidebar.tsx b/apps/client/src/components/settings/settings-sidebar.tsx index fe0c7e88..75e5a7af 100644 --- a/apps/client/src/components/settings/settings-sidebar.tsx +++ b/apps/client/src/components/settings/settings-sidebar.tsx @@ -12,15 +12,18 @@ import { IconLock, IconKey, IconWorld, + IconSparkles, } from "@tabler/icons-react"; import { Link, useLocation } from "react-router-dom"; import classes from "./settings.module.css"; import { useTranslation } from "react-i18next"; import { isCloud } from "@/lib/config.ts"; import useUserRole from "@/hooks/use-user-role.tsx"; -import { useAtom } from "jotai/index"; +import { useAtom } from "jotai"; import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts"; import { + prefetchApiKeyManagement, + prefetchApiKeys, prefetchBilling, prefetchGroups, prefetchLicense, @@ -60,6 +63,14 @@ const groupedData: DataGroup[] = [ icon: IconBrush, path: "/settings/account/preferences", }, + { + label: "API keys", + icon: IconKey, + path: "/settings/account/api-keys", + isCloud: true, + isEnterprise: true, + showDisabledInNonEE: true, + }, ], }, { @@ -90,6 +101,22 @@ const groupedData: DataGroup[] = [ { label: "Groups", icon: IconUsersGroup, path: "/settings/groups" }, { label: "Spaces", icon: IconSpaces, path: "/settings/spaces" }, { label: "Public sharing", icon: IconWorld, path: "/settings/sharing" }, + { + label: "API management", + icon: IconKey, + path: "/settings/api-keys", + isCloud: true, + isEnterprise: true, + isAdmin: true, + showDisabledInNonEE: true, + }, + { + label: "AI settings", + icon: IconSparkles, + path: "/settings/ai", + isAdmin: true, + isSelfhosted: true, + }, ], }, { @@ -195,6 +222,12 @@ export default function SettingsSidebar() { case "Public sharing": prefetchHandler = prefetchShares; break; + case "API keys": + prefetchHandler = prefetchApiKeys; + break; + case "API management": + prefetchHandler = prefetchApiKeyManagement; + break; default: break; } diff --git a/apps/client/src/ee/ai/components/ai-search-result.tsx b/apps/client/src/ee/ai/components/ai-search-result.tsx new file mode 100644 index 00000000..f082f25a --- /dev/null +++ b/apps/client/src/ee/ai/components/ai-search-result.tsx @@ -0,0 +1,113 @@ +import React, { useMemo } from "react"; +import { Paper, Text, Group, Stack, Loader, Box } from "@mantine/core"; +import { IconSparkles, IconFileText } from "@tabler/icons-react"; +import { Link } from "react-router-dom"; +import { IAiSearchResponse } from "../services/ai-search-service.ts"; +import { buildPageUrl } from "@/features/page/page.utils.ts"; +import { markdownToHtml } from "@docmost/editor-ext"; +import DOMPurify from "dompurify"; +import { useTranslation } from "react-i18next"; + +interface AiSearchResultProps { + result?: IAiSearchResponse; + isLoading?: boolean; + streamingAnswer?: string; + streamingSources?: any[]; +} + +export function AiSearchResult({ + result, + isLoading, + streamingAnswer = "", + streamingSources = [], +}: AiSearchResultProps) { + const { t } = useTranslation(); + + // Use streaming data if available, otherwise fall back to result + const answer = streamingAnswer || result?.answer || ""; + const sources = + streamingSources.length > 0 ? streamingSources : result?.sources || []; + + // Deduplicate sources by pageId, keeping the one with highest similarity + const deduplicatedSources = useMemo(() => { + if (!sources || sources.length === 0) return []; + + const pageMap = new Map(); + sources.forEach((source) => { + const existing = pageMap.get(source.pageId); + if (!existing || source.similarity > existing.similarity) { + pageMap.set(source.pageId, source); + } + }); + + return Array.from(pageMap.values()); + }, [sources]); + + if (isLoading && !answer) { + return ( + + + + {t("AI is thinking...")} + + + ); + } + + if (!answer && !isLoading) { + return null; + } + + return ( + + + + + + {t("AI Answer")} + + {isLoading && } + +
+ + + {deduplicatedSources.length > 0 && ( + + + {t("Sources")} + + {deduplicatedSources.map((source) => ( + + + + + + {source.title} + + + + + ))} + + )} + + ); +} diff --git a/apps/client/src/ee/ai/components/enable-ai-search.tsx b/apps/client/src/ee/ai/components/enable-ai-search.tsx new file mode 100644 index 00000000..53b0a9bd --- /dev/null +++ b/apps/client/src/ee/ai/components/enable-ai-search.tsx @@ -0,0 +1,69 @@ +import { Group, Text, Switch, MantineSize, Title } from "@mantine/core"; +import { useAtom } from "jotai"; +import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts"; +import { notifications } from "@mantine/notifications"; +import { isCloud } from "@/lib/config.ts"; +import useLicense from "@/ee/hooks/use-license.tsx"; + +export default function EnableAiSearch() { + const { t } = useTranslation(); + + return ( + <> + +
+ {t("AI-powered search (Ask AI)")} + + {t( + "AI search uses vector embeddings to provide semantic search capabilities across your workspace content.", + )} + +
+ + +
+ + ); +} + +interface AiSearchToggleProps { + size?: MantineSize; + label?: string; +} +export function AiSearchToggle({ size, label }: AiSearchToggleProps) { + const { t } = useTranslation(); + const [workspace, setWorkspace] = useAtom(workspaceAtom); + const [checked, setChecked] = useState(workspace?.settings?.ai?.search); + const { hasLicenseKey } = useLicense(); + + const hasAccess = isCloud() || (!isCloud() && hasLicenseKey); + + const handleChange = async (event: React.ChangeEvent) => { + const value = event.currentTarget.checked; + try { + const updatedWorkspace = await updateWorkspace({ aiSearch: value }); + setChecked(value); + setWorkspace(updatedWorkspace); + } catch (err) { + notifications.show({ + message: err?.response?.data?.message, + color: "red", + }); + } + }; + + return ( + + ); +} diff --git a/apps/client/src/ee/ai/hooks/use-ai-search.ts b/apps/client/src/ee/ai/hooks/use-ai-search.ts new file mode 100644 index 00000000..f9c5aa88 --- /dev/null +++ b/apps/client/src/ee/ai/hooks/use-ai-search.ts @@ -0,0 +1,46 @@ +import { useMutation, UseMutationResult } from "@tanstack/react-query"; +import { useState, useCallback } from "react"; +import { askAi, IAiSearchResponse } from "@/ee/ai/services/ai-search-service.ts"; +import { IPageSearchParams } from "@/features/search/types/search.types.ts"; + +// @ts-ignore +interface UseAiSearchResult extends UseMutationResult { + streamingAnswer: string; + streamingSources: any[]; + clearStreaming: () => void; +} + +export function useAiSearch(): UseAiSearchResult { + const [streamingAnswer, setStreamingAnswer] = useState(""); + const [streamingSources, setStreamingSources] = useState([]); + + const clearStreaming = useCallback(() => { + setStreamingAnswer(""); + setStreamingSources([]); + }, []); + + const mutation = useMutation({ + mutationFn: async (params: IPageSearchParams & { contentType?: string }) => { + setStreamingAnswer(""); + setStreamingSources([]); + + const { contentType, ...apiParams } = params; + + return await askAi(apiParams, (chunk) => { + if (chunk.content) { + setStreamingAnswer((prev) => prev + chunk.content); + } + if (chunk.sources) { + setStreamingSources(chunk.sources); + } + }); + }, + }); + + return { + ...mutation, + streamingAnswer, + streamingSources, + clearStreaming, + }; +} diff --git a/apps/client/src/ee/ai/hooks/use-ai.ts b/apps/client/src/ee/ai/hooks/use-ai.ts new file mode 100644 index 00000000..40c1ca12 --- /dev/null +++ b/apps/client/src/ee/ai/hooks/use-ai.ts @@ -0,0 +1,61 @@ +import { useState, useCallback, useRef } from "react"; +import { useAiGenerateStreamMutation } from "@/ee/ai/queries/ai-query.ts"; +import { AiGenerateDto } from "@/ee/ai/types/ai.types.ts"; + +export function useAiStream() { + const [content, setContent] = useState(""); + const [isStreaming, setIsStreaming] = useState(false); + const abortControllerRef = useRef(null); + const mutation = useAiGenerateStreamMutation(); + + const startStream = useCallback( + async (data: AiGenerateDto) => { + setContent(""); + setIsStreaming(true); + + try { + const controller = await mutation.mutateAsync({ + ...data, + onChunk: (chunk) => { + setContent((prev) => prev + chunk.content); + }, + onError: (error) => { + console.error("AI stream error:", error); + setIsStreaming(false); + }, + onComplete: () => { + setIsStreaming(false); + }, + }); + + abortControllerRef.current = controller; + } catch (error) { + console.error("Failed to start stream:", error); + setIsStreaming(false); + } + }, + [mutation] + ); + + const stopStream = useCallback(() => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; + setIsStreaming(false); + } + }, []); + + const resetContent = useCallback(() => { + setContent(""); + }, []); + + return { + content, + isStreaming, + startStream, + stopStream, + resetContent, + isLoading: mutation.isPending, + error: mutation.error, + }; +} \ No newline at end of file diff --git a/apps/client/src/ee/ai/pages/ai-settings.tsx b/apps/client/src/ee/ai/pages/ai-settings.tsx new file mode 100644 index 00000000..b9ab516d --- /dev/null +++ b/apps/client/src/ee/ai/pages/ai-settings.tsx @@ -0,0 +1,46 @@ +import { Helmet } from "react-helmet-async"; +import { getAppName, isCloud } from "@/lib/config.ts"; +import SettingsTitle from "@/components/settings/settings-title.tsx"; +import React from "react"; +import useUserRole from "@/hooks/use-user-role.tsx"; +import { useTranslation } from "react-i18next"; +import useLicense from "@/ee/hooks/use-license.tsx"; +import EnableAiSearch from "@/ee/ai/components/enable-ai-search.tsx"; +import { Alert } from "@mantine/core"; +import { IconInfoCircle } from "@tabler/icons-react"; + +export default function AiSettings() { + const { t } = useTranslation(); + const { isAdmin } = useUserRole(); + const { hasLicenseKey } = useLicense(); + + if (!isAdmin) { + return null; + } + + const hasAccess = isCloud() || (!isCloud() && hasLicenseKey); + + return ( + <> + + AI - {getAppName()} + + + + {!hasAccess && ( + } + title={t("Enterprise feature")} + color="blue" + mb="lg" + > + {t( + "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.", + )} + + )} + + + + ); +} diff --git a/apps/client/src/ee/ai/queries/ai-query.ts b/apps/client/src/ee/ai/queries/ai-query.ts new file mode 100644 index 00000000..076de9c7 --- /dev/null +++ b/apps/client/src/ee/ai/queries/ai-query.ts @@ -0,0 +1,44 @@ +import { + useMutation, + UseMutationResult, + useQuery, + UseQueryResult, +} from "@tanstack/react-query"; +import { + generateAiContent, + generateAiContentStream, +} from "@/ee/ai/services/ai-service.ts"; +import { + AiConfigResponse, + AiContentResponse, + AiGenerateDto, + AiStreamChunk, + AiStreamError, +} from "@/ee/ai/types/ai.types.ts"; + +export function useAiGenerateMutation(): UseMutationResult< + AiContentResponse, + Error, + AiGenerateDto +> { + return useMutation({ + mutationFn: (data: AiGenerateDto) => generateAiContent(data), + }); +} + +interface StreamCallbacks { + onChunk: (chunk: AiStreamChunk) => void; + onError?: (error: AiStreamError) => void; + onComplete?: () => void; +} + +export function useAiGenerateStreamMutation(): UseMutationResult< + AbortController, + Error, + AiGenerateDto & StreamCallbacks +> { + return useMutation({ + mutationFn: ({ onChunk, onError, onComplete, ...data }) => + generateAiContentStream(data, onChunk, onError, onComplete), + }); +} diff --git a/apps/client/src/ee/ai/services/ai-search-service.ts b/apps/client/src/ee/ai/services/ai-search-service.ts new file mode 100644 index 00000000..0254f5b2 --- /dev/null +++ b/apps/client/src/ee/ai/services/ai-search-service.ts @@ -0,0 +1,79 @@ +import api from "@/lib/api-client.ts"; +import { IPageSearchParams } from "@/features/search/types/search.types.ts"; + +export interface IAiSearchResponse { + answer: string; + sources?: Array<{ + pageId: string; + title: string; + slugId: string; + spaceSlug: string; + similarity: number; + distance: number; + chunkIndex: number; + excerpt: string; + }>; +} + +export async function askAi( + params: IPageSearchParams, + onChunk?: (chunk: { content?: string; sources?: any[] }) => void, +): Promise { + const response = await fetch("/api/ai/ask", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + body: JSON.stringify(params), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const reader = response.body?.getReader(); + const decoder = new TextDecoder(); + + let answer = ""; + let sources: any[] = []; + + if (reader) { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value); + const lines = chunk.split("\n"); + + for (const line of lines) { + if (line.startsWith("data: ")) { + const data = line.slice(6); + if (data === "[DONE]") break; + + try { + const parsed = JSON.parse(data); + if (parsed.error) { + throw new Error(parsed.error); + } + if (parsed.content) { + answer += parsed.content; + onChunk?.({ content: parsed.content }); + } + if (parsed.sources) { + sources = parsed.sources; + onChunk?.({ sources: parsed.sources }); + } + } catch (e) { + if (e instanceof Error) { + throw e; + } + // Skip invalid JSON + } + } + } + } + } + + return { answer, sources }; +} diff --git a/apps/client/src/ee/ai/services/ai-service.ts b/apps/client/src/ee/ai/services/ai-service.ts new file mode 100644 index 00000000..f3634d59 --- /dev/null +++ b/apps/client/src/ee/ai/services/ai-service.ts @@ -0,0 +1,89 @@ +import api from "@/lib/api-client.ts"; +import { + AiGenerateDto, + AiContentResponse, + AiStreamChunk, + AiStreamError, +} from "@/ee/ai/types/ai.types.ts"; + +export async function generateAiContent( + data: AiGenerateDto, +): Promise { + const req = await api.post("/ai/generate", data); + return req.data; +} + +export async function generateAiContentStream( + data: AiGenerateDto, + onChunk: (chunk: AiStreamChunk) => void, + onError?: (error: AiStreamError) => void, + onComplete?: () => void, +): Promise { + const abortController = new AbortController(); + try { + const response = await fetch("/api/ai/generate/stream", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + signal: abortController.signal, + credentials: "include", // This ensures cookies are sent, matching axios withCredentials + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const reader = response.body?.getReader(); + const decoder = new TextDecoder(); + + if (!reader) { + throw new Error("Response body is not readable"); + } + + const processStream = async () => { + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value, { stream: true }); + const lines = chunk.split("\n"); + + for (const line of lines) { + if (line.startsWith("data: ")) { + const data = line.slice(6); + if (data === "[DONE]") { + onComplete?.(); + return; + } + try { + const parsed = JSON.parse(data); + if (parsed.error) { + onError?.(parsed); + } else { + onChunk(parsed); + } + } catch (e) { + // Ignore parse errors for incomplete chunks + } + } + } + } + } catch (error) { + if (error.name !== "AbortError") { + onError?.({ error: error.message }); + } + } finally { + reader.releaseLock(); + } + }; + + processStream(); + } catch (error) { + onError?.({ error: error.message }); + } + + return abortController; +} diff --git a/apps/client/src/ee/ai/types/ai.types.ts b/apps/client/src/ee/ai/types/ai.types.ts new file mode 100644 index 00000000..a5fbc253 --- /dev/null +++ b/apps/client/src/ee/ai/types/ai.types.ts @@ -0,0 +1,40 @@ +export enum AiAction { + IMPROVE_WRITING = "improve_writing", + FIX_SPELLING_GRAMMAR = "fix_spelling_grammar", + MAKE_SHORTER = "make_shorter", + MAKE_LONGER = "make_longer", + SIMPLIFY = "simplify", + CHANGE_TONE = "change_tone", + SUMMARIZE = "summarize", + CONTINUE_WRITING = "continue_writing", + TRANSLATE = "translate", + CUSTOM = "custom", +} + +export interface AiGenerateDto { + action?: AiAction; + content: string; + prompt?: string; +} + +export interface AiContentResponse { + content: string; + usage?: { + promptTokens: number; + completionTokens: number; + totalTokens: number; + }; +} + +export interface AiConfigResponse { + configured: boolean; + availableActions: AiAction[]; +} + +export interface AiStreamChunk { + content: string; +} + +export interface AiStreamError { + error: string; +} diff --git a/apps/client/src/ee/api-key/components/api-key-created-modal.tsx b/apps/client/src/ee/api-key/components/api-key-created-modal.tsx new file mode 100644 index 00000000..6a01ee3c --- /dev/null +++ b/apps/client/src/ee/api-key/components/api-key-created-modal.tsx @@ -0,0 +1,72 @@ +import { + Modal, + Text, + Stack, + Alert, + Group, + Button, + TextInput, +} from "@mantine/core"; +import { IconAlertTriangle } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { IApiKey } from "@/ee/api-key"; +import CopyTextButton from "@/components/common/copy.tsx"; + +interface ApiKeyCreatedModalProps { + opened: boolean; + onClose: () => void; + apiKey: IApiKey; +} + +export function ApiKeyCreatedModal({ + opened, + onClose, + apiKey, +}: ApiKeyCreatedModalProps) { + const { t } = useTranslation(); + + if (!apiKey) return null; + + return ( + + + } + title={t("Important")} + color="red" + > + {t( + "Make sure to copy your API key now. You won't be able to see it again!", + )} + + +
+ + {t("API key")} + + + + + + +
+ + +
+
+ ); +} diff --git a/apps/client/src/ee/api-key/components/api-key-table.tsx b/apps/client/src/ee/api-key/components/api-key-table.tsx new file mode 100644 index 00000000..48757acc --- /dev/null +++ b/apps/client/src/ee/api-key/components/api-key-table.tsx @@ -0,0 +1,143 @@ +import { ActionIcon, Group, Menu, Table, Text } from "@mantine/core"; +import { IconDots, IconEdit, IconTrash } from "@tabler/icons-react"; +import { format } from "date-fns"; +import { useTranslation } from "react-i18next"; +import { IApiKey } from "@/ee/api-key"; +import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; +import React from "react"; +import NoTableResults from "@/components/common/no-table-results"; + +interface ApiKeyTableProps { + apiKeys: IApiKey[]; + isLoading?: boolean; + showUserColumn?: boolean; + onUpdate?: (apiKey: IApiKey) => void; + onRevoke?: (apiKey: IApiKey) => void; +} + +export function ApiKeyTable({ + apiKeys, + isLoading, + showUserColumn = false, + onUpdate, + onRevoke, +}: ApiKeyTableProps) { + const { t } = useTranslation(); + + const formatDate = (date: Date | string | null) => { + if (!date) return t("Never"); + return format(new Date(date), "MMM dd, yyyy"); + }; + + const isExpired = (expiresAt: string | null) => { + if (!expiresAt) return false; + return new Date(expiresAt) < new Date(); + }; + + return ( + + + + + {t("Name")} + {showUserColumn && {t("User")}} + {t("Last used")} + {t("Expires")} + {t("Created")} + + + + + + {apiKeys && apiKeys.length > 0 ? ( + apiKeys.map((apiKey: IApiKey, index: number) => ( + + + + {apiKey.name} + + + + {showUserColumn && apiKey.creator && ( + + + + + {apiKey.creator.name} + + + + )} + + + + {formatDate(apiKey.lastUsedAt)} + + + + + {apiKey.expiresAt ? ( + isExpired(apiKey.expiresAt) ? ( + + {t("Expired")} + + ) : ( + + {formatDate(apiKey.expiresAt)} + + ) + ) : ( + + {t("Never")} + + )} + + + + + {formatDate(apiKey.createdAt)} + + + + + + + + + + + + {onUpdate && ( + } + onClick={() => onUpdate(apiKey)} + > + {t("Rename")} + + )} + {onRevoke && ( + } + color="red" + onClick={() => onRevoke(apiKey)} + > + {t("Revoke")} + + )} + + + + + )) + ) : ( + + )} + +
+
+ ); +} diff --git a/apps/client/src/ee/api-key/components/create-api-key-modal.tsx b/apps/client/src/ee/api-key/components/create-api-key-modal.tsx new file mode 100644 index 00000000..cade36e8 --- /dev/null +++ b/apps/client/src/ee/api-key/components/create-api-key-modal.tsx @@ -0,0 +1,153 @@ +import { lazy, Suspense, useState } from "react"; +import { Modal, TextInput, Button, Group, Stack, Select } from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { zodResolver } from "mantine-form-zod-resolver"; +import { z } from "zod"; +import { useTranslation } from "react-i18next"; +import { useCreateApiKeyMutation } from "@/ee/api-key/queries/api-key-query"; +import { IconCalendar } from "@tabler/icons-react"; +import { IApiKey } from "@/ee/api-key"; + +const DateInput = lazy(() => + import("@mantine/dates").then((module) => ({ + default: module.DateInput, + })), +); + +interface CreateApiKeyModalProps { + opened: boolean; + onClose: () => void; + onSuccess: (response: IApiKey) => void; +} + +const formSchema = z.object({ + name: z.string().min(1, "Name is required"), + expiresAt: z.string().optional(), +}); +type FormValues = z.infer; + +export function CreateApiKeyModal({ + opened, + onClose, + onSuccess, +}: CreateApiKeyModalProps) { + const { t } = useTranslation(); + const [expirationOption, setExpirationOption] = useState("30"); + const createApiKeyMutation = useCreateApiKeyMutation(); + + const form = useForm({ + validate: zodResolver(formSchema), + initialValues: { + name: "", + expiresAt: "", + }, + }); + + const getExpirationDate = (): string | undefined => { + if (expirationOption === "never") { + return undefined; + } + if (expirationOption === "custom") { + return form.values.expiresAt; + } + const days = parseInt(expirationOption); + const date = new Date(); + date.setDate(date.getDate() + days); + return date.toISOString(); + }; + + const getExpirationLabel = (days: number) => { + const date = new Date(); + date.setDate(date.getDate() + days); + const formatted = date.toLocaleDateString("en-US", { + month: "short", + day: "2-digit", + year: "numeric", + }); + return `${days} days (${formatted})`; + }; + + const expirationOptions = [ + { value: "30", label: getExpirationLabel(30) }, + { value: "60", label: getExpirationLabel(60) }, + { value: "90", label: getExpirationLabel(90) }, + { value: "365", label: getExpirationLabel(365) }, + { value: "custom", label: t("Custom") }, + { value: "never", label: t("No expiration") }, + ]; + + const handleSubmit = async (data: { + name?: string; + expiresAt?: string | Date; + }) => { + const groupData = { + name: data.name, + expiresAt: getExpirationDate(), + }; + + try { + const createdKey = await createApiKeyMutation.mutateAsync(groupData); + onSuccess(createdKey); + form.reset(); + onClose(); + } catch (err) { + // + } + }; + + const handleClose = () => { + form.reset(); + setExpirationOption("30"); + onClose(); + }; + + return ( + +
handleSubmit(values))}> + + + +