Mats Klein commited on
Commit
8086ffb
1 Parent(s): d7fc51f

init ChatGPTFirewall

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.local.example +4 -0
  2. .eslintrc.cjs +18 -0
  3. .gitignore +24 -0
  4. .prettierrc +9 -0
  5. Dockerfile +62 -0
  6. README copy.md +30 -0
  7. entrypoint.sh +5 -0
  8. fly.toml +17 -0
  9. i18next-parser.config.js +5 -0
  10. index.html +15 -0
  11. nginx.conf +17 -0
  12. package-lock.json +0 -0
  13. package.json +49 -0
  14. public/favicon.ico +0 -0
  15. public/images/android-chrome-192x192.png +0 -0
  16. public/images/android-chrome-512x512.png +0 -0
  17. public/images/apple-touch-icon.png +0 -0
  18. public/images/favicon-16x16.png +0 -0
  19. public/images/favicon-32x32.png +0 -0
  20. public/locales/de/translation.json +86 -0
  21. public/locales/de/translation_old.json +37 -0
  22. public/locales/en/translation.json +86 -0
  23. public/locales/en/translation_old.json +38 -0
  24. src/App.tsx +30 -0
  25. src/api/Request.ts +28 -0
  26. src/api/fetchAPI.ts +23 -0
  27. src/api/fileApi.ts +35 -0
  28. src/api/messageApi.ts +24 -0
  29. src/api/roomsApi.ts +47 -0
  30. src/api/usersApi.ts +18 -0
  31. src/components/common/Cards/CustomCard.tsx +34 -0
  32. src/components/common/Cards/CustomCardStyles.tsx +52 -0
  33. src/components/common/Cards/ExampleCards/ExampleCards.tsx +64 -0
  34. src/components/common/Cards/ExampleCards/ExampleCardsStyles.tsx +29 -0
  35. src/components/common/Cards/GuideCards/GuideCards.tsx +32 -0
  36. src/components/common/Cards/GuideCards/GuideCardsStyles.tsx +28 -0
  37. src/components/common/ChatMessage/ChatMessage.tsx +0 -0
  38. src/components/common/FileExplorer/FileExplorer.tsx +179 -0
  39. src/components/common/FileExplorer/FileExplorerStyles.tsx +29 -0
  40. src/components/common/FileExplorer/NextCloudIcon.svg +3 -0
  41. src/components/common/FileExplorer/UploadButton.tsx +47 -0
  42. src/components/common/FileList/FileList.tsx +216 -0
  43. src/components/common/FileList/FileListStyles.tsx +23 -0
  44. src/components/common/FileSelector/FileSelector.tsx +126 -0
  45. src/components/common/FileSelector/FileSelectorStyles.tsx +29 -0
  46. src/components/common/LanguageSelector/LanguageSelector.tsx +85 -0
  47. src/components/common/LanguageSelector/LanguageSelectorStyles.tsx +14 -0
  48. src/components/common/LoginButton/LoginButton.tsx +33 -0
  49. src/components/common/LoginButton/LoginButtonStyles.tsx +14 -0
  50. src/components/common/MenuItem/MenuItem.tsx +0 -0
.env.local.example ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ VITE_AUTH0_DOMAIN=
2
+ VITE_AUTH0_CLIENT_ID=
3
+ VITE_JWT_AUDIENCE=
4
+ VITE_JWT_ISSUER=
.eslintrc.cjs ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ root: true,
3
+ env: { browser: true, es2020: true },
4
+ extends: [
5
+ 'eslint:recommended',
6
+ 'plugin:@typescript-eslint/recommended',
7
+ 'plugin:react-hooks/recommended'
8
+ ],
9
+ ignorePatterns: ['dist', '.eslintrc.cjs'],
10
+ parser: '@typescript-eslint/parser',
11
+ plugins: ['react-refresh'],
12
+ rules: {
13
+ 'react-refresh/only-export-components': [
14
+ 'warn',
15
+ { allowConstantExport: true }
16
+ ]
17
+ }
18
+ };
.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
.prettierrc ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "semi": true,
3
+ "singleQuote": true,
4
+ "printWidth": 80,
5
+ "tabWidth": 2,
6
+ "useTabs": false,
7
+ "trailingComma": "none",
8
+ "bracketSpacing": true
9
+ }
Dockerfile ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:current-bullseye-slim as dev
2
+
3
+ WORKDIR /app
4
+
5
+ COPY ./package.json .
6
+ COPY ./package-lock.json .
7
+
8
+ RUN npm install
9
+
10
+ RUN echo "VITE_AUTH0_DOMAIN=$VITE_AUTH0_DOMAIN" > .env.local && \
11
+ echo "VITE_AUTH0_CLIENT_ID=$VITE_AUTH0_CLIENT_ID" >> .env.local && \
12
+ echo "VITE_JWT_AUDIENCE=$VITE_JWT_AUDIENCE" >> .env.local && \
13
+ echo "VITE_JWT_ISSUER=$VITE_JWT_ISSUER" >> .env.local
14
+
15
+ COPY entrypoint.sh ./entrypoint.sh
16
+
17
+ RUN ["chmod", "+x", "entrypoint.sh"]
18
+ ENTRYPOINT ["/app/entrypoint.sh"]
19
+
20
+ FROM node:current-bullseye-slim as prod
21
+
22
+ ARG VITE_AUTH0_DOMAIN
23
+ ARG VITE_AUTH0_CLIENT_ID
24
+ ARG VITE_JWT_AUDIENCE
25
+ ARG VITE_JWT_ISSUER
26
+
27
+ ENV VITE_AUTH0_DOMAIN=$VITE_AUTH0_DOMAIN \
28
+ VITE_AUTH0_CLIENT_ID=$VITE_AUTH0_CLIENT_ID \
29
+ VITE_JWT_AUDIENCE=$VITE_JWT_AUDIENCE \
30
+ VITE_JWT_ISSUER=$VITE_JWT_ISSUER
31
+
32
+ WORKDIR /app
33
+
34
+ COPY package.json package-lock.json ./
35
+
36
+ RUN npm install
37
+
38
+ RUN echo "VITE_AUTH0_DOMAIN=$VITE_AUTH0_DOMAIN" > .env.production.local && \
39
+ echo "VITE_AUTH0_CLIENT_ID=$VITE_AUTH0_CLIENT_ID" >> .env.production.local && \
40
+ echo "VITE_JWT_AUDIENCE=$VITE_JWT_AUDIENCE" >> .env.production.local && \
41
+ echo "VITE_JWT_ISSUER=$VITE_JWT_ISSUER" >> .env.production.local
42
+
43
+ COPY . .
44
+
45
+ RUN npm run build
46
+
47
+ # Prod Stage
48
+ FROM nginx:stable-alpine
49
+
50
+ WORKDIR /usr/share/nginx/html
51
+
52
+ RUN rm -rf ./*
53
+
54
+ RUN mkdir -p /etc/nginx/ssl
55
+
56
+ COPY --from=prod /app/dist .
57
+
58
+ COPY nginx.conf /etc/nginx/conf.d/default.conf
59
+
60
+ EXPOSE 80
61
+
62
+ CMD ["nginx", "-g", "daemon off;"]
README copy.md ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + TypeScript + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9
+
10
+ ## Expanding the ESLint configuration
11
+
12
+ If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
13
+
14
+ - Configure the top-level `parserOptions` property like this:
15
+
16
+ ```js
17
+ export default {
18
+ // other rules...
19
+ parserOptions: {
20
+ ecmaVersion: 'latest',
21
+ sourceType: 'module',
22
+ project: ['./tsconfig.json', './tsconfig.node.json'],
23
+ tsconfigRootDir: __dirname
24
+ }
25
+ };
26
+ ```
27
+
28
+ - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
29
+ - Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
30
+ - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
entrypoint.sh ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ #!/bin/sh
2
+
3
+ cd /app
4
+ npm install
5
+ npm run dev
fly.toml ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # fly.toml app configuration file generated for ccc-frontend on 2023-08-21T15:35:09+02:00
2
+ #
3
+ # See https://fly.io/docs/reference/configuration/ for information about how to use this file.
4
+ #
5
+
6
+ app = "ccc-frontend"
7
+ primary_region = "ams"
8
+
9
+ [build]
10
+
11
+ [http_service]
12
+ internal_port = 443
13
+ force_https = true
14
+ auto_stop_machines = false
15
+ auto_start_machines = true
16
+ min_machines_running = 0
17
+ processes = ["app"]
i18next-parser.config.js ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ export default {
2
+ locales: ['en', 'de'],
3
+ output: 'public/locales/$LOCALE/translation.json',
4
+ input: 'src/**/*.{js,jsx,ts,tsx}'
5
+ };
index.html ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>ChatGPTfirewall</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ <script>
12
+ const global = globalThis;
13
+ </script>
14
+ </body>
15
+ </html>
nginx.conf ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ server {
2
+ listen 7860
3
+ server_name _
4
+
5
+ location / {
6
+ root /usr/share/nginx/html
7
+ try_files $uri $uri/ /index.html
8
+ }
9
+
10
+ location /api {
11
+ proxy_pass http://185.112.181.192:8000/api;
12
+ proxy_set_header X-Forwarded-Host $host;
13
+ proxy_set_header X-Real-IP $remote_addr;
14
+ proxy_set_header Host $host;
15
+ proxy_read_timeout 3600;
16
+ }
17
+ }
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build --mode production",
9
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10
+ "preview": "vite preview",
11
+ "i18n:extract": "npx i18next-parser",
12
+ "format": "prettier --write .",
13
+ "format:check": "prettier --check ."
14
+ },
15
+ "dependencies": {
16
+ "@auth0/auth0-react": "^2.2.4",
17
+ "@fluentui/react": "^8.115.5",
18
+ "@fluentui/react-components": "^9.46.3",
19
+ "@fluentui/react-icons": "^2.0.226",
20
+ "axios": "^1.6.7",
21
+ "date-fns": "^3.3.1",
22
+ "draft-js": "^0.11.7",
23
+ "i18next": "^23.8.2",
24
+ "i18next-browser-languagedetector": "^7.2.0",
25
+ "i18next-http-backend": "^2.4.3",
26
+ "prettier": "^3.2.4",
27
+ "react": "^18.2.0",
28
+ "react-country-flag": "^3.1.0",
29
+ "react-dom": "^18.2.0",
30
+ "react-highlight-within-textarea": "^3.2.1",
31
+ "react-i18next": "^14.0.1",
32
+ "react-router-dom": "^6.21.3"
33
+ },
34
+ "devDependencies": {
35
+ "@flydotio/dockerfile": "^0.5.2",
36
+ "@types/react": "^18.2.43",
37
+ "@types/react-dom": "^18.2.17",
38
+ "@typescript-eslint/eslint-plugin": "^6.14.0",
39
+ "@typescript-eslint/parser": "^6.20.0",
40
+ "@vitejs/plugin-react": "^4.2.1",
41
+ "eslint": "^8.55.0",
42
+ "eslint-plugin-react-hooks": "^4.6.0",
43
+ "eslint-plugin-react-refresh": "^0.4.5",
44
+ "i18next-parser": "^8.12.0",
45
+ "typescript": "^5.2.2",
46
+ "vite": "^5.0.8",
47
+ "vite-plugin-svgr": "^4.2.0"
48
+ }
49
+ }
public/favicon.ico ADDED
public/images/android-chrome-192x192.png ADDED
public/images/android-chrome-512x512.png ADDED
public/images/apple-touch-icon.png ADDED
public/images/favicon-16x16.png ADDED
public/images/favicon-32x32.png ADDED
public/locales/de/translation.json ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "guideCardStep1Title": "Daten hochladen",
3
+ "guideCardStep1Content": "Wähle Dateien aus oder lade sie hoch.",
4
+ "guideCardStep2Title": "Raum erstellen",
5
+ "guideCardStep2Content": "Richte einen Raum für deine Dateien ein.",
6
+ "guideCardStep3Title": "Fragen stellen",
7
+ "guideCardStep3Content": "Erhalte Antworten zu deinen Dokumenten.",
8
+ "guideCardDemoLinkText": "... oder probiere die Demo aus!",
9
+ "unexpectedErrorOccurred": "Ein unerwarteter Fehler ist aufgetreten",
10
+ "errorfetchingFiles": "Fehler beim Abrufen der Dateien",
11
+ "fileUploadSuccess": "Dateien erfolgreich hochgeladen",
12
+ "errorUploadingFiles": "Fehler beim hochladen der Dateien",
13
+ "filesDeletedSuccess": "Dateien erfolgreich entfernt",
14
+ "errorDeletingFiles": "Fehler beim Entfernen der Dateien",
15
+ "fileExplorerDialogButton": "Dateien",
16
+ "fileExplorerTitle": "Deine Dateien",
17
+ "nextCloudButtonSub": "Verbinden mit Nextcloud",
18
+ "nextCloudButton": "Nextcloud",
19
+ "loading": "Lade...",
20
+ "deleteSelectedFiles": "Löschen",
21
+ "dialogCloseButton": "Schließen",
22
+ "uploadButtonSub": "Manuell Dateien hochladen",
23
+ "uploadButton": "Hochladen",
24
+ "file": "Datei",
25
+ "language": "Sprache",
26
+ "fileSize": "Dateigröße",
27
+ "uploadedAt": "Hochgeladen am",
28
+ "FileSelectorTitle": "Wähle deine Dateien aus...",
29
+ "fileSelectorSelectButton": "Auswählen",
30
+ "errorSavingLanguage": "Fehler beim Speichern der Sprache",
31
+ "login": "Anmelden",
32
+ "chatGPTRoleName": "ChatGPT",
33
+ "trialOverMessage": "Ihre Testversion ist abgelaufen.",
34
+ "resultRoleName": "Antworten",
35
+ "searchMessageIntroText": "Hier finden Sie relevante Auszüge aus Ihren Dokumenten, die potenziell Antwort auf Ihre Frage geben könnten.",
36
+ "searchMessageStatText": "Ergebnis {{index}} aus \"{{filename}}\" mit einer Genauigkeit von {{accuracy}}%",
37
+ "searchMessageIntroHint": "Bitte stellen Sie sicher, dass alle sensiblen Informationen bereits identifiziert und entsprechend pseudonymisiert wurden, bevor Sie fortfahren.",
38
+ "cancelEditResultsButton": "Verwerfen",
39
+ "save": "Speichern",
40
+ "editResultsButton": "Bearbeiten",
41
+ "sendToChatGPTButton": "Senden",
42
+ "chatGPTSendHint": "Übermittelt Frage und Antworten pseudonymisiert an ChatGPT.",
43
+ "renameRoom": "Raum umbenennen",
44
+ "deleteRoom": "Raum löschen",
45
+ "errorFetchingRooms": "Fehler beim Abrufen der Räume",
46
+ "roomCreatedSuccessfully": "Raum erfolgreich erstellt",
47
+ "errorCreateRoom": "Fehler beim Erstellen des Raums",
48
+ "roomRenamedSuccessfully": "Raum erfolgreich umbenannt",
49
+ "errorRenameRoom": "Fehler beim Umbenennen des Raums",
50
+ "roomDeletedSuccessfully": "Raum erfolgreich gelöscht",
51
+ "errorDeleteRoom": "Fehler beim Löschen des Raums",
52
+ "createRoomButton": "Raum erstellen",
53
+ "roomNamePlaceholder": "Neuer Raumname...",
54
+ "settingsDrawerTitle": "Raum Einstellungen",
55
+ "promptTemplateLabel": "Prompt",
56
+ "promptTemplateSelectLabel": "Prompt Templates",
57
+ "promptTemplateOptionDE": "Prompt Template Deutsch",
58
+ "promptTemplateOptionEN": "Prompt Template Englisch",
59
+ "resultSentenceCountLabel": "Anzahl der Sätze vor und nach einem Ergebnis",
60
+ "actionSuccessful": "Aktion erfolgreich",
61
+ "actionFailed": "Aktion fehlgeschlagen",
62
+ "actionWarning": "Warnung",
63
+ "actionNotice": "Hinweis",
64
+ "tutorialCardsSubtitle": "Erstelle einen Raum und frage deine Dokumente",
65
+ "errorNoRoom": "Dieser Raum existiert nicht",
66
+ "errorUpdatingRoomDocuments": "Fehler beim Auswählen der Dateien",
67
+ "errorSendingMessage": "Fehler beim Senden der Nachricht",
68
+ "errorNotEnoughMessages": "Fehlende Nachrichten zum fortfahren",
69
+ "question": "Frage",
70
+ "context": "Kontext",
71
+ "errorSendingMessageToChatGPT": "Fehler beim Senden der Nachricht zu ChatGPT",
72
+ "isAnonymizedText": "Pseudonymisiert",
73
+ "isNotAnonymizedText": "Nicht Pseudonymisiert",
74
+ "exampleCardsSubtitle": "Testen Sie eine Beispielanfrage anhand der bereitgestellten Beispieldokumente",
75
+ "headerText": "ChatGPTfirewall",
76
+ "subheaderText": "Logge dich ein um ChatGPTfirewall auszuprobieren",
77
+ "documentation": "Dokumentation",
78
+ "landingPage": "Start",
79
+ "githubRepo": "Github",
80
+ "errorFetchingRoom": "Fehler beim Abrufen des Raums",
81
+ "settingsSavedSuccessfully": "Einstellungen erfolgreich gespeichert",
82
+ "errorSavingSettings": "Fehler beim Speichern der Einstellungen",
83
+ "emptyStateBodyTextStrong": "Keine Dateien ausgewählt",
84
+ "emptyStateBodyText": "Bitte wählen Sie die Dateien aus, die Sie für den Chat verwenden möchten.",
85
+ "addFilesEmptyPage": "Dateien auswählen"
86
+ }
public/locales/de/translation_old.json ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "1": {
3
+ " Upload your data or select it via your Cloud": ""
4
+ },
5
+ "2": {
6
+ " Ask questions that your data can answer": ""
7
+ },
8
+ "You can try out the demo": "",
9
+ "loginAndAskAnything": "",
10
+ "Logout": "Ausloggen",
11
+ "Login": "Einloggen",
12
+ "card1Upload": "",
13
+ "card2Ask": "",
14
+ "orText": "",
15
+ "card3Demo": "",
16
+ "logout": "Ausloggen",
17
+ "File": "",
18
+ "Language": "",
19
+ "File Size": "",
20
+ "tutorialCardStep1": "1. Laden Sie Ihre Daten hoch oder wählen Sie sie über Ihre Cloud aus",
21
+ "tutorialCardStep2": "2. Stellen Sie Fragen, die Ihre Daten beantworten können",
22
+ "tutorialCardDemoLink": "Sie können die Demo ausprobieren",
23
+ "or": "oder",
24
+ "guideCardDemoTitle": "Demo ausprobieren",
25
+ "guideCardDemoContent": "Nutze die Demo-Seite, um ein vorgefertigtes Beispiel mit Dateien und Fragen zu sehen.",
26
+ "guideCardDemoText": "",
27
+ "uploading": "Lade hoch...",
28
+ "uploadSuccess": "Upload erfolgreich",
29
+ "uploadError": "Upload fehlgeschlagen",
30
+ "demoPageChatWithData": "Demo Seite: Chatten Sie mit Ihren Daten",
31
+ "errorNoRoomSelected": "Es wurde kein Ru",
32
+ "errorNoFilesSelected": "",
33
+ "updatingRoomDocumentsSuccessfully": "",
34
+ "saveEditResultsButton": "Speichern",
35
+ "saveSettings": "Speichern",
36
+ "chatWithYourData": ""
37
+ }
public/locales/en/translation.json ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "guideCardStep1Title": "Upload Data",
3
+ "guideCardStep1Content": "Choose or upload files.",
4
+ "guideCardStep2Title": "Create Room",
5
+ "guideCardStep2Content": "Set up a room for your files.",
6
+ "guideCardStep3Title": "Ask Questions",
7
+ "guideCardStep3Content": "Get answers about your documents.",
8
+ "guideCardDemoLinkText": "...or test the demo!",
9
+ "unexpectedErrorOccurred": "An unexpected error occurred",
10
+ "errorfetchingFiles": "Error fetching files",
11
+ "fileUploadSuccess": "Files uploaded successfully",
12
+ "errorUploadingFiles": "Error uploading files",
13
+ "filesDeletedSuccess": "Files deleted successfully",
14
+ "errorDeletingFiles": "Error deleting files",
15
+ "fileExplorerDialogButton": "Files",
16
+ "fileExplorerTitle": "Your Files",
17
+ "nextCloudButtonSub": "Connect with Nextcloud",
18
+ "nextCloudButton": "Nextcloud",
19
+ "loading": "Loading...",
20
+ "deleteSelectedFiles": "Delete",
21
+ "dialogCloseButton": "Close",
22
+ "uploadButtonSub": "Upload files manually",
23
+ "uploadButton": "Upload",
24
+ "file": "File",
25
+ "language": "Language",
26
+ "fileSize": "File Size",
27
+ "uploadedAt": "Uploaded at",
28
+ "FileSelectorTitle": "Select your files...",
29
+ "fileSelectorSelectButton": "Select",
30
+ "errorSavingLanguage": "Error while saving language",
31
+ "login": "Login",
32
+ "chatGPTRoleName": "ChatGPT",
33
+ "trialOverMessage": "Your trial is over.",
34
+ "resultRoleName": "Answers",
35
+ "searchMessageIntroText": "Below are excerpts from your documents that may potentially answer your question.",
36
+ "searchMessageStatText": "Result {{index}} from \"{{filename}}\" with {{accuracy}}% accuracy.",
37
+ "searchMessageIntroHint": "Please ensure that all sensitive data have been identified and appropriately pseudonymized before proceeding.",
38
+ "cancelEditResultsButton": "Discard",
39
+ "save": "Save",
40
+ "editResultsButton": "Edit",
41
+ "sendToChatGPTButton": "Send",
42
+ "chatGPTSendHint": "Submits question and answers in pseudonymized form to ChatGPT.",
43
+ "renameRoom": "Rename Room",
44
+ "deleteRoom": "Delete Room",
45
+ "errorFetchingRooms": "Error fetching rooms",
46
+ "roomCreatedSuccessfully": "Room created successfully",
47
+ "errorCreateRoom": "Error creating room",
48
+ "roomRenamedSuccessfully": "Room renamed successfully",
49
+ "errorRenameRoom": "Error renaming room",
50
+ "roomDeletedSuccessfully": "Room deleted successfully",
51
+ "errorDeleteRoom": "Error deleting room",
52
+ "createRoomButton": "Create Room",
53
+ "roomNamePlaceholder": "New Roomname...",
54
+ "settingsDrawerTitle": "Room Settings",
55
+ "promptTemplateLabel": "Prompt",
56
+ "promptTemplateSelectLabel": "Prompt Templates",
57
+ "promptTemplateOptionDE": "Prompt Template German",
58
+ "promptTemplateOptionEN": "Prompt Template English",
59
+ "resultSentenceCountLabel": "Number of sentences before and after the result",
60
+ "actionSuccessful": "Action successful",
61
+ "actionFailed": "Action failed",
62
+ "actionWarning": "Warning",
63
+ "actionNotice": "Notice",
64
+ "tutorialCardsSubtitle": "Create a room and ask your documents",
65
+ "errorNoRoom": "This room doesn't exist",
66
+ "errorUpdatingRoomDocuments": "Error while selecting files",
67
+ "errorSendingMessage": "Error while sending message",
68
+ "errorNotEnoughMessages": "Missing messages to continue",
69
+ "question": "Question",
70
+ "context": "Context",
71
+ "errorSendingMessageToChatGPT": "Error while sending message to ChatGPT",
72
+ "isAnonymizedText": "Pseudonymized",
73
+ "isNotAnonymizedText": "Not Pseudonymized",
74
+ "exampleCardsSubtitle": "Try out a sample query using the provided example documents.",
75
+ "headerText": "ChatGPTfirewall",
76
+ "subheaderText": "Log in to start your session with ChatGPTFirewall.",
77
+ "documentation": "Documentation",
78
+ "landingPage": "Home",
79
+ "githubRepo": "Github",
80
+ "errorFetchingRoom": "Error fetching room",
81
+ "settingsSavedSuccessfully": "Settings saved successfully",
82
+ "errorSavingSettings": "Error while saving settings",
83
+ "emptyStateBodyTextStrong": "No Files Selected",
84
+ "emptyStateBodyText": "Please select the files you wish to use for chatting.",
85
+ "addFilesEmptyPage": "Select Files"
86
+ }
public/locales/en/translation_old.json ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "1": {
3
+ " Upload your data or select it via your Cloud": ""
4
+ },
5
+ "2": {
6
+ " Ask questions that your data can answer": ""
7
+ },
8
+ "You can try out the demo": "",
9
+ "loginAndAskAnything": "",
10
+ "Logout": "",
11
+ "Login": "",
12
+ "Login and ask anything": "",
13
+ "card1Upload": "",
14
+ "card2Ask": "",
15
+ "orText": "",
16
+ "card3Demo": "",
17
+ "logout": "Logout",
18
+ "File": "",
19
+ "Language": "",
20
+ "File Size": "",
21
+ "tutorialCardStep1": "1. Upload your data or select it via your Cloud",
22
+ "tutorialCardStep2": "2. Ask questions that your data can answer",
23
+ "tutorialCardDemoLink": "You can try out the demo",
24
+ "or": "or",
25
+ "guideCardDemoTitle": "Try the Demo",
26
+ "guideCardDemoContent": "Use the demo page to see a pre-selected example with files and questions.",
27
+ "guideCardDemoText": "",
28
+ "uploading": "Loading...",
29
+ "uploadSuccess": "Upload successful",
30
+ "uploadError": "Upload failed",
31
+ "demoPageChatWithData": "Demo Page: Chat with your data",
32
+ "errorNoRoomSelected": "",
33
+ "errorNoFilesSelected": "",
34
+ "updatingRoomDocumentsSuccessfully": "",
35
+ "saveEditResultsButton": "Save",
36
+ "saveSettings": "",
37
+ "chatWithYourData": ""
38
+ }
src/App.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
2
+
3
+ import Layout from './pages/Layout/Layout';
4
+ import Demo from './pages/Demo/Demo';
5
+ import Chat from './pages/Chat/Chat';
6
+ import Room from './pages/Room/Room';
7
+ import Page404 from './pages/Page404/Page404';
8
+ import ProtectedRoute from './components/common/ProtectedRoute/ProtectedRoute';
9
+
10
+ function App() {
11
+ return (
12
+ <Router>
13
+ <Routes>
14
+ <Route path="/" element={<Layout />}>
15
+ <Route element={<ProtectedRoute />}>
16
+ <Route index element={<Chat />} />
17
+ <Route path="demo" element={<Demo />} />
18
+ <Route path="chat">
19
+ <Route index element={<Chat />} />
20
+ <Route path="room/:id" element={<Room />} />
21
+ </Route>
22
+ </Route>
23
+ <Route path="*" element={<Page404 />} />
24
+ </Route>
25
+ </Routes>
26
+ </Router>
27
+ );
28
+ }
29
+
30
+ export default App;
src/api/Request.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
2
+
3
+ async function Request<T>(config: AxiosRequestConfig): Promise<T> {
4
+ try {
5
+ // Accessing token for subsequent requests
6
+ const token = localStorage.getItem('userToken');
7
+ if (token == null) {
8
+ console.group('get token');
9
+ } else {
10
+ axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
11
+ }
12
+ const response: AxiosResponse<T> = await axios(config);
13
+ return response.data;
14
+ } catch (error) {
15
+ if (axios.isAxiosError(error)) {
16
+ console.error(
17
+ `API call failed with status: ${error.response?.status}`,
18
+ error.message
19
+ );
20
+ throw new Error(`API call failed: ${error.response?.status}`);
21
+ } else {
22
+ console.error('An unexpected error occurred', error);
23
+ throw error;
24
+ }
25
+ }
26
+ }
27
+
28
+ export default Request;
src/api/fetchAPI.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ async function fetchAPI<TBody, TResponse>(
2
+ url: string,
3
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'POST',
4
+ body?: TBody,
5
+ headers?: HeadersInit
6
+ ): Promise<TResponse> {
7
+ const response = await fetch(url, {
8
+ method,
9
+ headers: {
10
+ 'Content-Type': 'application/json',
11
+ ...headers
12
+ },
13
+ body: JSON.stringify(body)
14
+ });
15
+
16
+ if (!response.ok) {
17
+ throw new Error(`HTTP error! Status: ${response.status}`);
18
+ }
19
+
20
+ return response.json() as TResponse;
21
+ }
22
+
23
+ export default fetchAPI;
src/api/fileApi.ts ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Request from './Request';
2
+ import { File } from '../models/File';
3
+
4
+ export async function getFiles(auth0_id: string): Promise<File[]> {
5
+ return Request<File[]>({
6
+ url: '/api/documents',
7
+ method: 'POST',
8
+ headers: {
9
+ 'Content-Type': 'application/json'
10
+ },
11
+ data: JSON.stringify({
12
+ auth0_id: auth0_id
13
+ })
14
+ });
15
+ }
16
+
17
+ export const getFile = (documentID: string): Promise<File> => {
18
+ return Request<File>({ url: `/api/documents/${documentID}`, method: 'GET' });
19
+ };
20
+
21
+ export async function createFiles(data: FormData): Promise<File[]> {
22
+ return Request<File[]>({
23
+ url: '/api/documents/upload',
24
+ method: 'POST',
25
+ data: data
26
+ });
27
+ }
28
+
29
+ export function deleteFiles(fileIds: string[]): Promise<void> {
30
+ return Request<void>({
31
+ url: '/api/documents',
32
+ method: 'DELETE',
33
+ data: fileIds
34
+ });
35
+ }
src/api/messageApi.ts ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Message } from '../models/Message';
2
+ import Request from './Request';
3
+
4
+ export const createSearchMessage = (message: Message): Promise<Message> => {
5
+ return Request({
6
+ url: '/api/messages/search',
7
+ method: 'POST',
8
+ data: message
9
+ });
10
+ };
11
+
12
+ export const createChatGPTMessage = (
13
+ message: Message,
14
+ demo?: boolean
15
+ ): Promise<Message> => {
16
+ const baseUrl = '/api/messages/chatgpt';
17
+ const url = demo ? `${baseUrl}?demo=true` : baseUrl;
18
+
19
+ return Request({
20
+ url: url,
21
+ method: 'POST',
22
+ data: message
23
+ });
24
+ };
src/api/roomsApi.ts ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Room } from '../models/Room';
2
+ import Request from './Request';
3
+
4
+ export const getRooms = (userAuth0Id: string): Promise<Room[]> => {
5
+ return Request<Room[]>({
6
+ url: `/api/rooms?user_auth0_id=${userAuth0Id}`,
7
+ method: 'GET'
8
+ });
9
+ };
10
+
11
+ export const getRoom = (roomId: string): Promise<Room> => {
12
+ return Request<Room>({ url: `/api/rooms/${roomId}/`, method: 'GET' });
13
+ };
14
+
15
+ export const createRoom = (
16
+ user_auth0_id: string,
17
+ room_name: string
18
+ ): Promise<Room> => {
19
+ return Request<Room>({
20
+ url: '/api/rooms',
21
+ method: 'POST',
22
+ data: { user_auth0_id: user_auth0_id, room_name: room_name }
23
+ });
24
+ };
25
+
26
+ export const updateRoom = (room: Room): Promise<Room> => {
27
+ return Request<Room>({
28
+ url: `/api/rooms/${room.id}/`,
29
+ method: 'PUT',
30
+ data: room
31
+ });
32
+ };
33
+
34
+ export const updateRoomFiles = (
35
+ roomId: string,
36
+ documentIds: string[]
37
+ ): Promise<Room> => {
38
+ return Request<Room>({
39
+ url: `/api/rooms/${roomId}/documents/`,
40
+ method: 'POST',
41
+ data: { document_ids: documentIds }
42
+ });
43
+ };
44
+
45
+ export const deleteRoom = (roomId: string): Promise<Room> => {
46
+ return Request<Room>({ url: `/api/rooms/${roomId}/`, method: 'DELETE' });
47
+ };
src/api/usersApi.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { User } from '../models/User';
2
+ import Request from './Request';
3
+
4
+ export const getUser = async (auth0_id: string): Promise<User> => {
5
+ return Request<User>({ url: `/api/user/${auth0_id}/`, method: 'GET' });
6
+ };
7
+
8
+ export const createUser = async (user: User): Promise<User> => {
9
+ return Request<User>({ url: '/api/user/', method: 'POST', data: user });
10
+ };
11
+
12
+ export const updateUser = async (user: User): Promise<User> => {
13
+ return Request<User>({
14
+ url: `/api/user/${user.auth0_id}/`,
15
+ method: 'PUT',
16
+ data: user
17
+ });
18
+ };
src/components/common/Cards/CustomCard.tsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Divider } from '@fluentui/react-components';
2
+ import CustomCardStyles from './CustomCardStyles';
3
+
4
+ interface CustomCardProps {
5
+ title?: string;
6
+ step?: string;
7
+ value?: string;
8
+ onClick?: (value?: string) => void;
9
+ children?: React.ReactNode;
10
+ }
11
+
12
+ const CustomCard = ({
13
+ step,
14
+ title,
15
+ value,
16
+ onClick,
17
+ children
18
+ }: CustomCardProps) => {
19
+ const styles = CustomCardStyles();
20
+ return (
21
+ <div className={styles.guideCard} onClick={() => onClick?.(value)}>
22
+ <div>
23
+ <div className={styles.header}>
24
+ <div className={styles.step}>{step}</div>
25
+ <div className={styles.title}>{title}</div>
26
+ </div>
27
+ <Divider />
28
+ <div className={styles.content}> {children}</div>
29
+ </div>
30
+ </div>
31
+ );
32
+ };
33
+
34
+ export default CustomCard;
src/components/common/Cards/CustomCardStyles.tsx ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { makeStyles, tokens, shorthands } from '@fluentui/react-components';
2
+
3
+ const CustomCardStyles = makeStyles({
4
+ guideCard: {
5
+ boxShadow: tokens.shadow16,
6
+ width: '250px',
7
+ minHeight: '100px',
8
+ display: 'flex',
9
+ flexDirection: 'column',
10
+ ...shorthands.margin(tokens.spacingVerticalXL, tokens.spacingHorizontalXL),
11
+ ...shorthands.padding(tokens.spacingVerticalL, tokens.spacingHorizontalL),
12
+ ...shorthands.borderRadius(tokens.borderRadiusLarge),
13
+ ':hover': {
14
+ boxShadow: tokens.shadow28,
15
+ '& .step': {
16
+ backgroundColor: tokens.colorBrandBackground,
17
+ color: tokens.colorNeutralBackground1
18
+ }
19
+ }
20
+ },
21
+ step: {
22
+ ...shorthands.padding(
23
+ tokens.spacingVerticalXXS,
24
+ tokens.spacingHorizontalXXS
25
+ ),
26
+ fontSize: tokens.fontSizeBase600,
27
+ fontWeight: tokens.fontWeightBold,
28
+ color: tokens.colorBrandBackground,
29
+ backgroundColor: tokens.colorNeutralBackground1,
30
+ ...shorthands.borderRadius(tokens.borderRadiusCircular),
31
+ width: '32px',
32
+ height: '32px',
33
+ display: 'flex',
34
+ justifyContent: 'center',
35
+ alignItems: 'center',
36
+ marginRight: tokens.spacingHorizontalM
37
+ },
38
+ title: {
39
+ fontSize: tokens.fontSizeBase500,
40
+ fontWeight: tokens.fontWeightSemibold
41
+ },
42
+ content: {
43
+ fontSize: tokens.fontSizeBase400,
44
+ ...shorthands.margin(tokens.spacingVerticalS, 0)
45
+ },
46
+ header: {
47
+ display: 'flex',
48
+ alignItems: 'center'
49
+ }
50
+ });
51
+
52
+ export default CustomCardStyles;
src/components/common/Cards/ExampleCards/ExampleCards.tsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useTranslation } from 'react-i18next';
2
+ import CustomCard from '../CustomCard';
3
+ import ExampleCardsStyles from './ExampleCardsStyles';
4
+
5
+ export type Example = {
6
+ text: string;
7
+ value: string;
8
+ };
9
+
10
+ const EXAMPLES_DE: Example[] = [
11
+ {
12
+ text: 'Was ist auf der Weinversteigerung in Trier passiert?',
13
+ value: 'Was ist auf der Weinversteigerung in Trier passiert?'
14
+ },
15
+ {
16
+ text: 'Worüber ist der Edelmann E erbost?',
17
+ value: 'Worüber ist der Edelmann E erbost??'
18
+ },
19
+ {
20
+ text: 'Warum wurde B die Einreise in die USA verweigert?',
21
+ value: 'Warum wurde B die Einreise in die USA verweigert?'
22
+ }
23
+ ];
24
+
25
+ const EXAMPLES_EN: Example[] = [
26
+ {
27
+ text: 'What did Detective Miller discover?',
28
+ value: 'What did Detective Miller discover?'
29
+ },
30
+ {
31
+ text: 'What stories does Willow have?',
32
+ value: 'What stories does Willow have?'
33
+ },
34
+ {
35
+ text: "How many people were in Max's group? ",
36
+ value: "How many people were in Max's group?"
37
+ }
38
+ ];
39
+
40
+ interface ExampleCardsProps {
41
+ onExampleClicked: (value?: string) => void;
42
+ }
43
+
44
+ export const ExampleCards = ({ onExampleClicked }: ExampleCardsProps) => {
45
+ const styles = ExampleCardsStyles();
46
+ const { i18n } = useTranslation();
47
+ const examples = i18n.language === 'de' ? EXAMPLES_DE : EXAMPLES_EN;
48
+ return (
49
+ <div className={styles.examplesCardList}>
50
+ {examples.map((example, i) => (
51
+ <CustomCard
52
+ key={i}
53
+ step={`${i + 1}.`}
54
+ value={example.value}
55
+ onClick={onExampleClicked}
56
+ >
57
+ {example.text}
58
+ </CustomCard>
59
+ ))}
60
+ </div>
61
+ );
62
+ };
63
+
64
+ export default ExampleCards;
src/components/common/Cards/ExampleCards/ExampleCardsStyles.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { makeStyles, tokens, shorthands } from '@fluentui/react-components';
2
+
3
+ const ExampleCardsStyles = makeStyles({
4
+ container: {
5
+ display: 'flex',
6
+ flexDirection: 'column'
7
+ },
8
+ examplesCardList: {
9
+ display: 'flex',
10
+ flexWrap: 'wrap',
11
+ ...shorthands.gap('10px'),
12
+
13
+ justifyContent: 'center'
14
+ },
15
+ demoTextContainer: {
16
+ textAlign: 'center', // Zentriert den Text und den Link
17
+ marginTop: tokens.spacingVerticalM, // Gibt etwas Abstand nach oben
18
+ marginBottom: tokens.spacingVerticalM // Gibt etwas Abstand nach unten
19
+ },
20
+ demoLink: {
21
+ fontSize: tokens.fontSizeBase400, // Etwas größere Schrift für den Link
22
+ fontWeight: tokens.fontWeightSemibold, // Macht den Link fett
23
+ color: tokens.colorBrandBackground, // Farbe für den Link, nutze Brand-Farben für Klickbarkeit
24
+ ...shorthands.margin(tokens.spacingVerticalS, 0),
25
+ paddingBottom: '65px'
26
+ }
27
+ });
28
+
29
+ export default ExampleCardsStyles;
src/components/common/Cards/GuideCards/GuideCards.tsx ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Link } from 'react-router-dom';
2
+ import { useTranslation } from 'react-i18next';
3
+ import CustomCard from '../CustomCard';
4
+ import GuideCardsStyles from './GuideCardsStyles';
5
+
6
+ const GuideCards = () => {
7
+ const styles = GuideCardsStyles();
8
+ const { t } = useTranslation();
9
+
10
+ return (
11
+ <div className={styles.container}>
12
+ <div className={styles.guideCardList}>
13
+ <CustomCard step="1." title={t('guideCardStep1Title')}>
14
+ {t('guideCardStep1Content')}
15
+ </CustomCard>
16
+ <CustomCard step="2." title={t('guideCardStep2Title')}>
17
+ {t('guideCardStep2Content')}
18
+ </CustomCard>
19
+ <CustomCard step="3." title={t('guideCardStep3Title')}>
20
+ {t('guideCardStep3Content')}
21
+ </CustomCard>
22
+ </div>
23
+ <div className={styles.demoTextContainer}>
24
+ <Link to="/demo" className={styles.demoLink}>
25
+ {t('guideCardDemoLinkText')}
26
+ </Link>
27
+ </div>
28
+ </div>
29
+ );
30
+ };
31
+
32
+ export default GuideCards;
src/components/common/Cards/GuideCards/GuideCardsStyles.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { makeStyles, tokens, shorthands } from '@fluentui/react-components';
2
+
3
+ const GuideCardsStyles = makeStyles({
4
+ container: {
5
+ display: 'flex',
6
+ flexDirection: 'column'
7
+ },
8
+ guideCardList: {
9
+ display: 'flex',
10
+ flexWrap: 'wrap',
11
+ ...shorthands.gap('10px'),
12
+ justifyContent: 'center'
13
+ },
14
+ demoTextContainer: {
15
+ textAlign: 'center', // Zentriert den Text und den Link
16
+ marginTop: tokens.spacingVerticalM, // Gibt etwas Abstand nach oben
17
+ marginBottom: tokens.spacingVerticalM // Gibt etwas Abstand nach unten
18
+ },
19
+ demoLink: {
20
+ fontSize: tokens.fontSizeBase400, // Etwas größere Schrift für den Link
21
+ fontWeight: tokens.fontWeightSemibold, // Macht den Link fett
22
+ color: tokens.colorBrandBackground, // Farbe für den Link, nutze Brand-Farben für Klickbarkeit
23
+ ...shorthands.margin(tokens.spacingVerticalS, 0),
24
+ paddingBottom: '65px'
25
+ }
26
+ });
27
+
28
+ export default GuideCardsStyles;
src/components/common/ChatMessage/ChatMessage.tsx ADDED
File without changes
src/components/common/FileExplorer/FileExplorer.tsx ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { KeyboardEvent, MouseEvent, useEffect, useState } from 'react';
2
+ import {
3
+ Button,
4
+ Dialog,
5
+ DialogActions,
6
+ DialogBody,
7
+ DialogContent,
8
+ DialogSurface,
9
+ DialogTitle,
10
+ DialogTrigger,
11
+ CompoundButton,
12
+ Spinner,
13
+ OnSelectionChangeData
14
+ } from '@fluentui/react-components';
15
+ import { DocumentFolder24Regular } from '@fluentui/react-icons';
16
+ import { useTranslation } from 'react-i18next';
17
+ import FileExplorerStyles from './FileExplorerStyles';
18
+ import { FileList as FileListComp } from '../FileList/FileList';
19
+ import NextCloudIcon from './NextCloudIcon.svg?react';
20
+ import UploadButton from './UploadButton';
21
+ import { useUser } from '../../../context/UserProvider';
22
+ import { File as FileType } from '../../../models/File';
23
+ import { getFiles, createFiles, deleteFiles } from '../../../api/fileApi';
24
+ import { useToast } from '../../../context/ToastProvider';
25
+
26
+ export const FileExplorer = () => {
27
+ const styles = FileExplorerStyles();
28
+ const { t } = useTranslation();
29
+ const { user } = useUser();
30
+ const { showToast } = useToast();
31
+ const [files, setFiles] = useState<FileType[]>([]);
32
+ const [selectedFiles, setSelectedFiles] = useState(new Set());
33
+ const [isLoading, setIsLoading] = useState<boolean>(true);
34
+
35
+ useEffect(() => {
36
+ if (!user) return;
37
+
38
+ const getAllDocuments = () => {
39
+ setIsLoading(true);
40
+ getFiles(user.auth0_id)
41
+ .then((response) => {
42
+ setFiles(response);
43
+ })
44
+ .catch((error) => {
45
+ const errorMessage =
46
+ error.response?.data?.error || t('unexpectedErrorOccurred');
47
+ showToast(`${t('errorfetchingFiles')}: ${errorMessage}`, 'error');
48
+ })
49
+ .finally(() => {
50
+ setIsLoading(false);
51
+ });
52
+ };
53
+
54
+ getAllDocuments();
55
+ }, [user, showToast, t]);
56
+
57
+ const handleFileUpload = (selectedFiles: FileList) => {
58
+ const formData = new FormData();
59
+ const tempFiles: FileType[] = [];
60
+
61
+ for (const rawFile of selectedFiles) {
62
+ formData.append('files', rawFile);
63
+ tempFiles.push({
64
+ filename: rawFile.name,
65
+ fileSize: rawFile.size,
66
+ isUploading: true
67
+ });
68
+ }
69
+
70
+ setFiles((prevFiles) => [...prevFiles, ...tempFiles]);
71
+ formData.append('user', user!.auth0_id as string);
72
+
73
+ createFiles(formData)
74
+ .then((response) => {
75
+ setFiles((prevFiles) =>
76
+ prevFiles.map((file) => {
77
+ const updatedFile = response.find(
78
+ (resFile) => resFile.filename === file.filename
79
+ );
80
+ return updatedFile ? { ...updatedFile, isUploading: false } : file;
81
+ })
82
+ );
83
+ showToast(t('fileUploadSuccess'), 'success');
84
+ })
85
+ .catch((error) => {
86
+ setFiles((prevFiles) => prevFiles.filter((file) => !file.isUploading));
87
+ const errorMessage =
88
+ error.response?.data?.error || t('unexpectedErrorOccurred');
89
+ showToast(`${t('errorUploadingFiles')}: ${errorMessage}`, 'error');
90
+ });
91
+ };
92
+
93
+ const handleSelectedFiles = (
94
+ _e: MouseEvent | KeyboardEvent,
95
+ data: OnSelectionChangeData
96
+ ) => {
97
+ const selectedFileIds = [...data.selectedItems];
98
+ setSelectedFiles(new Set(selectedFileIds));
99
+ };
100
+
101
+ const deleteSelectedFiles = () => {
102
+ const fileIdsToDelete: string[] = Array.from(selectedFiles).map(
103
+ (id) => id as string
104
+ );
105
+
106
+ deleteFiles(fileIdsToDelete)
107
+ .then(() => {
108
+ setFiles((prevFiles) =>
109
+ prevFiles.filter((file) => !selectedFiles.has(file.id))
110
+ );
111
+ setSelectedFiles(new Set());
112
+ showToast(t('filesDeletedSuccess'), 'success');
113
+ })
114
+ .catch((error) => {
115
+ const errorMessage =
116
+ error.response?.data?.error || t('unexpectedErrorOccurred');
117
+ showToast(`${t('errorDeletingFiles')}: ${errorMessage}`, 'error');
118
+ });
119
+ };
120
+
121
+ return (
122
+ <Dialog>
123
+ <DialogTrigger disableButtonEnhancement>
124
+ <Button
125
+ appearance="primary"
126
+ icon={<DocumentFolder24Regular />}
127
+ iconPosition="after"
128
+ size="large"
129
+ className={styles.triggerButton}
130
+ >
131
+ {t('fileExplorerDialogButton')}
132
+ </Button>
133
+ </DialogTrigger>
134
+ <DialogSurface className={styles.surface}>
135
+ <DialogBody>
136
+ <DialogTitle>{t('fileExplorerTitle')}</DialogTitle>
137
+ <DialogContent className={styles.content}>
138
+ <div className={styles.actionButtons}>
139
+ <UploadButton onFilesSelected={handleFileUpload} />
140
+ <CompoundButton
141
+ icon={<NextCloudIcon />}
142
+ appearance="subtle"
143
+ size="small"
144
+ secondaryContent={t('nextCloudButtonSub')}
145
+ disabled
146
+ >
147
+ {t('nextCloudButton')}
148
+ </CompoundButton>
149
+ </div>
150
+ {isLoading ? (
151
+ <Spinner label={t('loading')} />
152
+ ) : (
153
+ <div className={styles.fileList}>
154
+ <Button
155
+ disabled={selectedFiles.size === 0}
156
+ onClick={deleteSelectedFiles}
157
+ className={styles.deleteButton}
158
+ >
159
+ {t('deleteSelectedFiles')}
160
+ </Button>
161
+ <FileListComp
162
+ files={files}
163
+ onSelectionChange={handleSelectedFiles}
164
+ />
165
+ </div>
166
+ )}
167
+ </DialogContent>
168
+ <DialogActions>
169
+ <DialogTrigger disableButtonEnhancement>
170
+ <Button appearance="secondary">{t('dialogCloseButton')}</Button>
171
+ </DialogTrigger>
172
+ </DialogActions>
173
+ </DialogBody>
174
+ </DialogSurface>
175
+ </Dialog>
176
+ );
177
+ };
178
+
179
+ export default FileExplorer;
src/components/common/FileExplorer/FileExplorerStyles.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { makeStyles, tokens } from '@fluentui/react-components';
2
+
3
+ const FileExplorerStyles = makeStyles({
4
+ triggerButton: {
5
+ fontWeight: tokens.fontWeightMedium
6
+ },
7
+ surface: {
8
+ maxWidth: '60%',
9
+ minWidth: '25%'
10
+ },
11
+ content: {
12
+ display: 'flex',
13
+ flexDirection: 'column'
14
+ },
15
+ actionButtons: {
16
+ display: 'flex',
17
+ justifyContent: 'space-around',
18
+ marginBottom: tokens.spacingVerticalL
19
+ },
20
+ fileList: {
21
+ display: 'flex',
22
+ flexDirection: 'column'
23
+ },
24
+ deleteButton: {
25
+ alignSelf: 'end'
26
+ }
27
+ });
28
+
29
+ export default FileExplorerStyles;
src/components/common/FileExplorer/NextCloudIcon.svg ADDED
src/components/common/FileExplorer/UploadButton.tsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { CompoundButton } from '@fluentui/react-components';
2
+ import { ArrowUpload24Regular } from '@fluentui/react-icons';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { useRef, ChangeEvent } from 'react';
5
+
6
+ interface UploadButtonProps {
7
+ onFilesSelected: (selectedFiles: FileList) => void;
8
+ }
9
+
10
+ export const UploadButton = ({ onFilesSelected }: UploadButtonProps) => {
11
+ const { t } = useTranslation();
12
+ const hiddenFileInput = useRef<HTMLInputElement | null>(null);
13
+
14
+ const handleClick = () => {
15
+ if (hiddenFileInput.current) {
16
+ hiddenFileInput.current.click();
17
+ }
18
+ };
19
+
20
+ const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
21
+ if (event.target.files && event.target.files.length > 0) {
22
+ onFilesSelected(event.target.files);
23
+ }
24
+ };
25
+
26
+ return (
27
+ <CompoundButton
28
+ icon={<ArrowUpload24Regular />}
29
+ appearance="subtle"
30
+ size="small"
31
+ secondaryContent={t('uploadButtonSub')}
32
+ onClick={handleClick}
33
+ >
34
+ {t('uploadButton')}
35
+ <input
36
+ type="file"
37
+ style={{ display: 'none' }}
38
+ ref={hiddenFileInput}
39
+ onChange={handleFileChange}
40
+ multiple
41
+ accept=".pdf,.docx,.doc,.txt,.rtf,.html,.xml,.csv,.md"
42
+ />
43
+ </CompoundButton>
44
+ );
45
+ };
46
+
47
+ export default UploadButton;
src/components/common/FileList/FileList.tsx ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ DataGrid,
3
+ DataGridBody,
4
+ DataGridCell,
5
+ DataGridHeader,
6
+ DataGridHeaderCell,
7
+ DataGridRow,
8
+ OnSelectionChangeData,
9
+ SelectionItemId,
10
+ Spinner,
11
+ TableCellLayout,
12
+ TableColumnDefinition,
13
+ createTableColumn
14
+ } from '@fluentui/react-components';
15
+ import FileListStyles from './FileListStyles';
16
+ import { File } from '../../../models/File';
17
+ import { useTranslation } from 'react-i18next';
18
+ import ReactCountryFlag from 'react-country-flag';
19
+ import { format } from 'date-fns';
20
+ import { KeyboardEvent, MouseEvent } from 'react';
21
+
22
+ const langToCountryCode = (lang: string) => {
23
+ const map: { [key: string]: string } = {
24
+ de: 'DE',
25
+ en: 'US'
26
+ };
27
+
28
+ return map[lang] || 'US';
29
+ };
30
+
31
+ const formatFileSize = (size: number): string => {
32
+ if (size < 1024) return size + ' Bytes';
33
+ else if (size < 1024 * 1024) return (size / 1024).toFixed(1) + ' KB';
34
+ else if (size < 1024 * 1024 * 1024)
35
+ return (size / 1024 / 1024).toFixed(1) + ' MB';
36
+ else return (size / 1024 / 1024 / 1024).toFixed(1) + ' GB';
37
+ };
38
+
39
+ interface FileListProps {
40
+ files: File[];
41
+ selectedFileIds?: Set<SelectionItemId>;
42
+ onSelectionChange?: (
43
+ e: MouseEvent | KeyboardEvent,
44
+ data: OnSelectionChangeData
45
+ ) => void;
46
+ }
47
+
48
+ export const FileList = ({
49
+ files,
50
+ selectedFileIds,
51
+ onSelectionChange
52
+ }: FileListProps) => {
53
+ const styles = FileListStyles();
54
+ const { t } = useTranslation();
55
+
56
+ const columns: TableColumnDefinition<File>[] = [
57
+ createTableColumn<File>({
58
+ columnId: 'isUploading',
59
+ renderHeaderCell: () => {
60
+ return '';
61
+ },
62
+ renderCell: (file) => {
63
+ return file.isUploading ? <Spinner size="tiny" /> : null;
64
+ }
65
+ }),
66
+ createTableColumn<File>({
67
+ columnId: 'file',
68
+ compare: (a, b) => {
69
+ return a.filename.localeCompare(b.filename);
70
+ },
71
+ renderHeaderCell: () => {
72
+ return t('file');
73
+ },
74
+ renderCell: (file) => {
75
+ const [fileName, fileExtension] = file.filename.split('.');
76
+ const file_icon = `https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/assets/item-types/16/${fileExtension}.svg`;
77
+
78
+ return (
79
+ <TableCellLayout truncate className={styles.fileCell}>
80
+ <img
81
+ src={file_icon}
82
+ className={styles.fileIcon}
83
+ alt={`${fileExtension} file icon`}
84
+ />
85
+ <span className={styles.fileCellText}>{fileName}</span>
86
+ </TableCellLayout>
87
+ );
88
+ }
89
+ }),
90
+ createTableColumn<File>({
91
+ columnId: 'lang',
92
+ compare: (a, b) => {
93
+ const langA = a.lang || '';
94
+ const langB = b.lang || '';
95
+ return langA.localeCompare(langB);
96
+ },
97
+ renderHeaderCell: () => t('language'),
98
+ renderCell: (file) =>
99
+ file.lang ? (
100
+ <TableCellLayout
101
+ media={
102
+ <ReactCountryFlag
103
+ countryCode={langToCountryCode(file.lang)}
104
+ svg
105
+ className={styles.langIcon}
106
+ title={file.lang}
107
+ />
108
+ }
109
+ >
110
+ {file.lang.toUpperCase()}
111
+ </TableCellLayout>
112
+ ) : null
113
+ }),
114
+ createTableColumn<File>({
115
+ columnId: 'size',
116
+ compare: (a, b) => {
117
+ return a.fileSize - b.fileSize;
118
+ },
119
+ renderHeaderCell: () => {
120
+ return t('fileSize');
121
+ },
122
+ renderCell: (file) => {
123
+ return (
124
+ <TableCellLayout>{formatFileSize(file.fileSize)}</TableCellLayout>
125
+ );
126
+ }
127
+ }),
128
+ createTableColumn<File>({
129
+ columnId: 'uploadedAt',
130
+ compare: (a, b) => {
131
+ const dateA = a.uploadedAt ? a.uploadedAt.getTime() : 0;
132
+ const dateB = b.uploadedAt ? b.uploadedAt.getTime() : 0;
133
+ return dateB - dateA;
134
+ },
135
+ renderHeaderCell: () => t('uploadedAt'),
136
+ renderCell: (file) =>
137
+ file.uploadedAt ? (
138
+ <TableCellLayout>
139
+ {format(file.uploadedAt, 'dd.MM.yyyy HH:mm')}
140
+ </TableCellLayout>
141
+ ) : null
142
+ })
143
+ ];
144
+
145
+ const columnSizingOptions = {
146
+ isUploading: {
147
+ defaultWidth: 20,
148
+ minWidth: 20,
149
+ idealWidth: 20
150
+ },
151
+ file: {
152
+ defaultWidth: 240,
153
+ minWidth: 60,
154
+ idealWidth: 240
155
+ },
156
+ lang: {
157
+ defaultWidth: 80,
158
+ minWidth: 60,
159
+ idealWidth: 80
160
+ },
161
+ size: {
162
+ defaultWidth: 90,
163
+ minWidth: 90,
164
+ idealWidth: 90
165
+ },
166
+ uploadedAt: {
167
+ defaultWidth: 120,
168
+ minWidth: 120,
169
+ idealWidth: 120
170
+ }
171
+ };
172
+
173
+ return (
174
+ <DataGrid
175
+ items={files}
176
+ columns={columns}
177
+ sortable
178
+ subtleSelection
179
+ selectionMode="multiselect"
180
+ resizableColumns
181
+ columnSizingOptions={columnSizingOptions}
182
+ getRowId={(item) => item.id}
183
+ focusMode="composite"
184
+ selectedItems={selectedFileIds}
185
+ onSelectionChange={onSelectionChange}
186
+ >
187
+ <DataGridHeader>
188
+ <DataGridRow
189
+ selectionCell={{
190
+ checkboxIndicator: { 'aria-label': 'Select all rows' }
191
+ }}
192
+ >
193
+ {({ renderHeaderCell }) => (
194
+ <DataGridHeaderCell>{renderHeaderCell()}</DataGridHeaderCell>
195
+ )}
196
+ </DataGridRow>
197
+ </DataGridHeader>
198
+ <DataGridBody<File>>
199
+ {({ item, rowId }) => (
200
+ <DataGridRow<File>
201
+ key={rowId}
202
+ selectionCell={{
203
+ checkboxIndicator: { 'aria-label': 'Select row' }
204
+ }}
205
+ >
206
+ {({ renderCell }) => (
207
+ <DataGridCell>{renderCell(item)}</DataGridCell>
208
+ )}
209
+ </DataGridRow>
210
+ )}
211
+ </DataGridBody>
212
+ </DataGrid>
213
+ );
214
+ };
215
+
216
+ export default FileList;
src/components/common/FileList/FileListStyles.tsx ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { makeStyles, tokens } from '@fluentui/react-components';
2
+
3
+ const FileListStyles = makeStyles({
4
+ fileIcon: {
5
+ verticalAlign: 'middle',
6
+ maxHeight: '16px',
7
+ maxWidth: '16px',
8
+ marginRight: tokens.spacingHorizontalS
9
+ },
10
+ langIcon: {
11
+ verticalAlign: 'middle',
12
+ maxHeight: '16px',
13
+ maxWidth: '16px'
14
+ },
15
+ fileCell: {
16
+ display: 'flex'
17
+ },
18
+ fileCellText: {
19
+ width: '100%'
20
+ }
21
+ });
22
+
23
+ export default FileListStyles;
src/components/common/FileSelector/FileSelector.tsx ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { KeyboardEvent, MouseEvent, useEffect, useState } from 'react';
2
+ import {
3
+ Button,
4
+ Dialog,
5
+ DialogActions,
6
+ DialogBody,
7
+ DialogContent,
8
+ DialogSurface,
9
+ DialogTitle,
10
+ DialogTrigger,
11
+ Spinner,
12
+ OnSelectionChangeData,
13
+ SelectionItemId,
14
+ DialogTriggerChildProps
15
+ } from '@fluentui/react-components';
16
+ import { useTranslation } from 'react-i18next';
17
+ import FileSelectorStyles from './FileSelectorStyles';
18
+ import { FileList as FileListComp } from '../FileList/FileList';
19
+ import { useUser } from '../../../context/UserProvider';
20
+ import { File, File as FileType } from '../../../models/File';
21
+ import { getFiles } from '../../../api/fileApi';
22
+ import { User } from '../../../models/User';
23
+ interface FileSelectorProps {
24
+ onFilesSelected: (files: File[]) => void;
25
+ selectedFiles: File[];
26
+ triggerButton: (props: DialogTriggerChildProps) => JSX.Element;
27
+ passedUser?: User;
28
+ }
29
+
30
+ export const FileSelector = ({
31
+ onFilesSelected,
32
+ selectedFiles,
33
+ triggerButton,
34
+ passedUser
35
+ }: FileSelectorProps) => {
36
+ const styles = FileSelectorStyles();
37
+ const { t } = useTranslation();
38
+ const { user: hookUser } = useUser();
39
+ const [files, setFiles] = useState<FileType[]>([]);
40
+ const [selectedFileIds, setSelectedFileIds] = useState<Set<SelectionItemId>>(
41
+ new Set()
42
+ );
43
+ const [isLoading, setIsLoading] = useState<boolean>(true);
44
+ const [dialogOpen, setDialogState] = useState<boolean>(false);
45
+
46
+ const user = passedUser || hookUser;
47
+
48
+ useEffect(() => {
49
+ if (!user || !dialogOpen) return;
50
+
51
+ const getAllDocuments = () => {
52
+ setIsLoading(true);
53
+ getFiles(user.auth0_id)
54
+ .then((response) => {
55
+ setFiles(response);
56
+ })
57
+ .catch((error) => {
58
+ console.error('Error fetching documents:', error);
59
+ })
60
+ .finally(() => {
61
+ setIsLoading(false);
62
+ });
63
+ };
64
+
65
+ getAllDocuments();
66
+ }, [user, dialogOpen]);
67
+
68
+ useEffect(() => {
69
+ const preSelectedIds = new Set(
70
+ selectedFiles
71
+ .filter((file) => file.id !== undefined)
72
+ .map((file) => file.id!)
73
+ );
74
+ setSelectedFileIds(preSelectedIds);
75
+ }, [selectedFiles]);
76
+
77
+ const handleSelectedFiles = (
78
+ _e: MouseEvent | KeyboardEvent,
79
+ data: OnSelectionChangeData
80
+ ) => {
81
+ setSelectedFileIds(new Set(data.selectedItems));
82
+ };
83
+
84
+ const selectFiles = () => {
85
+ const selectedFiles = files.filter((file) => selectedFileIds.has(file.id!));
86
+ onFilesSelected(selectedFiles);
87
+ setDialogState(false);
88
+ };
89
+
90
+ return (
91
+ <Dialog
92
+ open={dialogOpen}
93
+ onOpenChange={(_event, data) => setDialogState(data.open)}
94
+ >
95
+ <DialogTrigger disableButtonEnhancement>{triggerButton}</DialogTrigger>
96
+ <DialogSurface className={styles.surface}>
97
+ <DialogBody>
98
+ <DialogTitle>{t('FileSelectorTitle')}</DialogTitle>
99
+ <DialogContent className={styles.content}>
100
+ {isLoading ? (
101
+ <Spinner label={t('loading')} />
102
+ ) : (
103
+ <div className={styles.fileList}>
104
+ <FileListComp
105
+ selectedFileIds={selectedFileIds}
106
+ files={files}
107
+ onSelectionChange={handleSelectedFiles}
108
+ />
109
+ </div>
110
+ )}
111
+ </DialogContent>
112
+ <DialogActions>
113
+ <DialogTrigger disableButtonEnhancement>
114
+ <Button appearance="secondary">{t('dialogCloseButton')}</Button>
115
+ </DialogTrigger>
116
+ <Button appearance="primary" onClick={selectFiles}>
117
+ {t('fileSelectorSelectButton')}
118
+ </Button>
119
+ </DialogActions>
120
+ </DialogBody>
121
+ </DialogSurface>
122
+ </Dialog>
123
+ );
124
+ };
125
+
126
+ export default FileSelector;
src/components/common/FileSelector/FileSelectorStyles.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { makeStyles, tokens } from '@fluentui/react-components';
2
+
3
+ const FileSelectorStyles = makeStyles({
4
+ triggerButton: {
5
+ fontWeight: tokens.fontWeightMedium
6
+ },
7
+ surface: {
8
+ maxWidth: '60%',
9
+ minWidth: '25%'
10
+ },
11
+ content: {
12
+ display: 'flex',
13
+ flexDirection: 'column'
14
+ },
15
+ actionButtons: {
16
+ display: 'flex',
17
+ justifyContent: 'space-around',
18
+ marginBottom: tokens.spacingVerticalL
19
+ },
20
+ fileList: {
21
+ display: 'flex',
22
+ flexDirection: 'column'
23
+ },
24
+ deleteButton: {
25
+ alignSelf: 'end'
26
+ }
27
+ });
28
+
29
+ export default FileSelectorStyles;
src/components/common/LanguageSelector/LanguageSelector.tsx ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import {
4
+ Dropdown,
5
+ Option,
6
+ OptionOnSelectData,
7
+ SelectionEvents
8
+ } from '@fluentui/react-components';
9
+ import LanguageSelectorStyles from './LanguageSelectorStyles';
10
+ import { useUser } from '../../../context/UserProvider';
11
+ import { updateUser } from '../../../api/usersApi';
12
+ import { useToast } from '../../../context/ToastProvider';
13
+
14
+ const LanguageSelector = () => {
15
+ const styles = LanguageSelectorStyles();
16
+ const { t, i18n } = useTranslation();
17
+ const { user, setUser } = useUser();
18
+ const { showToast } = useToast();
19
+
20
+ const options = [
21
+ { value: 'en', text: 'EN' },
22
+ { value: 'de', text: 'DE' }
23
+ ];
24
+
25
+ const [selectedLanguage, setSelectedLanguage] = useState(() => {
26
+ return user?.lang || localStorage.getItem('cgfwLanguage') || i18n.language;
27
+ });
28
+
29
+ const defaultOption = options.find(
30
+ (option) => option.value === selectedLanguage,
31
+ { value: 'en', text: 'EN' }
32
+ );
33
+
34
+ useEffect(() => {
35
+ const storedLanguage = localStorage.getItem('cgfwLanguage');
36
+ if (storedLanguage && storedLanguage !== i18n.language) {
37
+ i18n.changeLanguage(storedLanguage);
38
+ }
39
+ }, [i18n]);
40
+
41
+ const handleChangeLanguage = (
42
+ _: SelectionEvents,
43
+ data: OptionOnSelectData
44
+ ) => {
45
+ const newLanguage = data.optionValue;
46
+ if (newLanguage && newLanguage !== selectedLanguage) {
47
+ setSelectedLanguage(newLanguage);
48
+ localStorage.setItem('cgfwLanguage', newLanguage);
49
+ i18n.changeLanguage(newLanguage);
50
+
51
+ if (user) {
52
+ const updatedUser = { ...user, lang: newLanguage };
53
+ updateUser(updatedUser)
54
+ .then((userResponse) => {
55
+ setUser(userResponse);
56
+ })
57
+ .catch((error) => {
58
+ const errorMessage =
59
+ error.response?.data?.error || t('unexpectedErrorOccurred');
60
+ showToast(`${t('errorSavingLanguage')}: ${errorMessage}`, 'error');
61
+ });
62
+ }
63
+ }
64
+ };
65
+
66
+ return (
67
+ <Dropdown
68
+ defaultValue={defaultOption?.text}
69
+ onOptionSelect={handleChangeLanguage}
70
+ className={styles.dropdown}
71
+ >
72
+ {options.map((option) => (
73
+ <Option
74
+ key={option.value}
75
+ value={option.value}
76
+ className={styles.option}
77
+ >
78
+ {option.text}
79
+ </Option>
80
+ ))}
81
+ </Dropdown>
82
+ );
83
+ };
84
+
85
+ export default LanguageSelector;
src/components/common/LanguageSelector/LanguageSelectorStyles.tsx ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { makeStyles } from '@fluentui/react-components';
2
+
3
+ const LanguageSelectorStyles = makeStyles({
4
+ dropdown: {
5
+ width: 'auto',
6
+ minWidth: '32px'
7
+ },
8
+ option: {
9
+ width: 'auto',
10
+ minWidth: '32px'
11
+ }
12
+ });
13
+
14
+ export default LanguageSelectorStyles;
src/components/common/LoginButton/LoginButton.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useAuth0 } from '@auth0/auth0-react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import LoginButtonStyles from './LoginButtonStyles';
4
+ import { Button } from '@fluentui/react-components';
5
+ import { SignOut24Regular } from '@fluentui/react-icons';
6
+
7
+ const LoginButton = () => {
8
+ const styles = LoginButtonStyles();
9
+ const { loginWithRedirect, logout, isAuthenticated, user } = useAuth0();
10
+ const { t } = useTranslation();
11
+
12
+ if (isAuthenticated) {
13
+ return (
14
+ <div className={styles.loginButtonContainer}>
15
+ <div className={styles.userText}>{user?.email}</div>
16
+ <Button
17
+ appearance="primary"
18
+ icon={<SignOut24Regular />}
19
+ size="large"
20
+ onClick={() => logout()}
21
+ />
22
+ </div>
23
+ );
24
+ } else {
25
+ return (
26
+ <Button appearance="primary" onClick={() => loginWithRedirect()}>
27
+ {t('login')}
28
+ </Button>
29
+ );
30
+ }
31
+ };
32
+
33
+ export default LoginButton;
src/components/common/LoginButton/LoginButtonStyles.tsx ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { makeStyles, tokens } from '@fluentui/react-components';
2
+
3
+ const LoginButtonStyles = makeStyles({
4
+ userText: {
5
+ fontWeight: tokens.fontWeightMedium,
6
+ marginRight: tokens.spacingHorizontalL
7
+ },
8
+ loginButtonContainer: {
9
+ display: 'flex',
10
+ alignItems: 'center'
11
+ }
12
+ });
13
+
14
+ export default LoginButtonStyles;
src/components/common/MenuItem/MenuItem.tsx ADDED
File without changes