Spaces:
Runtime error
Runtime error
Mats Klein
commited on
Commit
•
8086ffb
1
Parent(s):
d7fc51f
init ChatGPTFirewall
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .env.local.example +4 -0
- .eslintrc.cjs +18 -0
- .gitignore +24 -0
- .prettierrc +9 -0
- Dockerfile +62 -0
- README copy.md +30 -0
- entrypoint.sh +5 -0
- fly.toml +17 -0
- i18next-parser.config.js +5 -0
- index.html +15 -0
- nginx.conf +17 -0
- package-lock.json +0 -0
- package.json +49 -0
- public/favicon.ico +0 -0
- public/images/android-chrome-192x192.png +0 -0
- public/images/android-chrome-512x512.png +0 -0
- public/images/apple-touch-icon.png +0 -0
- public/images/favicon-16x16.png +0 -0
- public/images/favicon-32x32.png +0 -0
- public/locales/de/translation.json +86 -0
- public/locales/de/translation_old.json +37 -0
- public/locales/en/translation.json +86 -0
- public/locales/en/translation_old.json +38 -0
- src/App.tsx +30 -0
- src/api/Request.ts +28 -0
- src/api/fetchAPI.ts +23 -0
- src/api/fileApi.ts +35 -0
- src/api/messageApi.ts +24 -0
- src/api/roomsApi.ts +47 -0
- src/api/usersApi.ts +18 -0
- src/components/common/Cards/CustomCard.tsx +34 -0
- src/components/common/Cards/CustomCardStyles.tsx +52 -0
- src/components/common/Cards/ExampleCards/ExampleCards.tsx +64 -0
- src/components/common/Cards/ExampleCards/ExampleCardsStyles.tsx +29 -0
- src/components/common/Cards/GuideCards/GuideCards.tsx +32 -0
- src/components/common/Cards/GuideCards/GuideCardsStyles.tsx +28 -0
- src/components/common/ChatMessage/ChatMessage.tsx +0 -0
- src/components/common/FileExplorer/FileExplorer.tsx +179 -0
- src/components/common/FileExplorer/FileExplorerStyles.tsx +29 -0
- src/components/common/FileExplorer/NextCloudIcon.svg +3 -0
- src/components/common/FileExplorer/UploadButton.tsx +47 -0
- src/components/common/FileList/FileList.tsx +216 -0
- src/components/common/FileList/FileListStyles.tsx +23 -0
- src/components/common/FileSelector/FileSelector.tsx +126 -0
- src/components/common/FileSelector/FileSelectorStyles.tsx +29 -0
- src/components/common/LanguageSelector/LanguageSelector.tsx +85 -0
- src/components/common/LanguageSelector/LanguageSelectorStyles.tsx +14 -0
- src/components/common/LoginButton/LoginButton.tsx +33 -0
- src/components/common/LoginButton/LoginButtonStyles.tsx +14 -0
- 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
|