mirror of
https://github.com/suitenumerique/docs.git
synced 2026-05-08 08:02:15 +02:00
Compare commits
15 Commits
e2e-hackat
...
feature/ai
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fa8ca0cd8 | ||
|
|
55785e9d14 | ||
|
|
3fe432bf61 | ||
|
|
e370a23e85 | ||
|
|
83045f5d28 | ||
|
|
3883d80ccb | ||
|
|
08a1e3c0e4 | ||
|
|
d118f9249d | ||
|
|
a584e9eeb0 | ||
|
|
ef8dd98b7b | ||
|
|
ae82d137c2 | ||
|
|
77282907cb | ||
|
|
c7d425b651 | ||
|
|
e63e92e731 | ||
|
|
36c0b3dda2 |
278
.github/workflows/impress.yml
vendored
278
.github/workflows/impress.yml
vendored
@@ -9,52 +9,52 @@ on:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
# lint-git:
|
||||
# runs-on: ubuntu-latest
|
||||
# if: github.event_name == 'pull_request' # Makes sense only for pull requests
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@v2
|
||||
# with:
|
||||
# fetch-depth: 0
|
||||
# - name: show
|
||||
# run: git log
|
||||
# - name: Enforce absence of print statements in code
|
||||
# run: |
|
||||
# ! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude)**/impress.yml' | grep "print("
|
||||
# - name: Check absence of fixup commits
|
||||
# run: |
|
||||
# ! git log | grep 'fixup!'
|
||||
# - name: Install gitlint
|
||||
# run: pip install --user requests gitlint
|
||||
# - name: Lint commit messages added to main
|
||||
# run: ~/.local/bin/gitlint --commits origin/${{ github.event.pull_request.base.ref }}..HEAD
|
||||
lint-git:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request' # Makes sense only for pull requests
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: show
|
||||
run: git log
|
||||
- name: Enforce absence of print statements in code
|
||||
run: |
|
||||
! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude)**/impress.yml' | grep "print("
|
||||
- name: Check absence of fixup commits
|
||||
run: |
|
||||
! git log | grep 'fixup!'
|
||||
- name: Install gitlint
|
||||
run: pip install --user requests gitlint
|
||||
- name: Lint commit messages added to main
|
||||
run: ~/.local/bin/gitlint --commits origin/${{ github.event.pull_request.base.ref }}..HEAD
|
||||
|
||||
# check-changelog:
|
||||
# runs-on: ubuntu-latest
|
||||
# if: |
|
||||
# contains(github.event.pull_request.labels.*.name, 'noChangeLog') == false &&
|
||||
# github.event_name == 'pull_request'
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@v3
|
||||
# with:
|
||||
# fetch-depth: 50
|
||||
# - name: Check that the CHANGELOG has been modified in the current branch
|
||||
# run: git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.after }} | grep 'CHANGELOG.md'
|
||||
check-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
contains(github.event.pull_request.labels.*.name, 'noChangeLog') == false &&
|
||||
github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Check that the CHANGELOG has been modified in the current branch
|
||||
run: git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.after }} | grep 'CHANGELOG.md'
|
||||
|
||||
# lint-changelog:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@v2
|
||||
# - name: Check CHANGELOG max line length
|
||||
# run: |
|
||||
# max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
|
||||
# if [ $max_line_length -ge 80 ]; then
|
||||
# echo "ERROR: CHANGELOG has lines longer than 80 characters."
|
||||
# exit 1
|
||||
# fi
|
||||
lint-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Check CHANGELOG max line length
|
||||
run: |
|
||||
max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
|
||||
if [ $max_line_length -ge 80 ]; then
|
||||
echo "ERROR: CHANGELOG has lines longer than 80 characters."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
build-mails:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -96,112 +96,112 @@ jobs:
|
||||
path: "src/backend/core/templates/mail"
|
||||
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
|
||||
# lint-back:
|
||||
# runs-on: ubuntu-latest
|
||||
# defaults:
|
||||
# run:
|
||||
# working-directory: src/backend
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@v2
|
||||
# - name: Install Python
|
||||
# uses: actions/setup-python@v3
|
||||
# with:
|
||||
# python-version: "3.10"
|
||||
# - name: Install development dependencies
|
||||
# run: pip install --user .[dev]
|
||||
# - name: Check code formatting with ruff
|
||||
# run: ~/.local/bin/ruff format . --diff
|
||||
# - name: Lint code with ruff
|
||||
# run: ~/.local/bin/ruff check .
|
||||
# - name: Lint code with pylint
|
||||
# run: ~/.local/bin/pylint .
|
||||
lint-back:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/backend
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- name: Install development dependencies
|
||||
run: pip install --user .[dev]
|
||||
- name: Check code formatting with ruff
|
||||
run: ~/.local/bin/ruff format . --diff
|
||||
- name: Lint code with ruff
|
||||
run: ~/.local/bin/ruff check .
|
||||
- name: Lint code with pylint
|
||||
run: ~/.local/bin/pylint .
|
||||
|
||||
# test-back:
|
||||
# runs-on: ubuntu-latest
|
||||
# needs: build-mails
|
||||
test-back:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-mails
|
||||
|
||||
# defaults:
|
||||
# run:
|
||||
# working-directory: src/backend
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src/backend
|
||||
|
||||
# services:
|
||||
# postgres:
|
||||
# image: postgres:16
|
||||
# env:
|
||||
# POSTGRES_DB: impress
|
||||
# POSTGRES_USER: dinum
|
||||
# POSTGRES_PASSWORD: pass
|
||||
# ports:
|
||||
# - 5432:5432
|
||||
# # needed because the postgres container does not provide a healthcheck
|
||||
# options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_DB: impress
|
||||
POSTGRES_USER: dinum
|
||||
POSTGRES_PASSWORD: pass
|
||||
ports:
|
||||
- 5432:5432
|
||||
# needed because the postgres container does not provide a healthcheck
|
||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
|
||||
# env:
|
||||
# DJANGO_CONFIGURATION: Test
|
||||
# DJANGO_SETTINGS_MODULE: impress.settings
|
||||
# DJANGO_SECRET_KEY: ThisIsAnExampleKeyForTestPurposeOnly
|
||||
# OIDC_OP_JWKS_ENDPOINT: /endpoint-for-test-purpose-only
|
||||
# DB_HOST: localhost
|
||||
# DB_NAME: impress
|
||||
# DB_USER: dinum
|
||||
# DB_PASSWORD: pass
|
||||
# DB_PORT: 5432
|
||||
# STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
|
||||
# AWS_S3_ENDPOINT_URL: http://localhost:9000
|
||||
# AWS_S3_ACCESS_KEY_ID: impress
|
||||
# AWS_S3_SECRET_ACCESS_KEY: password
|
||||
env:
|
||||
DJANGO_CONFIGURATION: Test
|
||||
DJANGO_SETTINGS_MODULE: impress.settings
|
||||
DJANGO_SECRET_KEY: ThisIsAnExampleKeyForTestPurposeOnly
|
||||
OIDC_OP_JWKS_ENDPOINT: /endpoint-for-test-purpose-only
|
||||
DB_HOST: localhost
|
||||
DB_NAME: impress
|
||||
DB_USER: dinum
|
||||
DB_PASSWORD: pass
|
||||
DB_PORT: 5432
|
||||
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
|
||||
AWS_S3_ENDPOINT_URL: http://localhost:9000
|
||||
AWS_S3_ACCESS_KEY_ID: impress
|
||||
AWS_S3_SECRET_ACCESS_KEY: password
|
||||
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@v4
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# - name: Create writable /data
|
||||
# run: |
|
||||
# sudo mkdir -p /data/media && \
|
||||
# sudo mkdir -p /data/static
|
||||
- name: Create writable /data
|
||||
run: |
|
||||
sudo mkdir -p /data/media && \
|
||||
sudo mkdir -p /data/static
|
||||
|
||||
# - name: Restore the mail templates
|
||||
# uses: actions/cache@v4
|
||||
# id: mail-templates
|
||||
# with:
|
||||
# path: "src/backend/core/templates/mail"
|
||||
# key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
- name: Restore the mail templates
|
||||
uses: actions/cache@v4
|
||||
id: mail-templates
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
|
||||
# - name: Start Minio
|
||||
# run: |
|
||||
# docker pull minio/minio
|
||||
# docker run -d --name minio \
|
||||
# -p 9000:9000 \
|
||||
# -e "MINIO_ACCESS_KEY=impress" \
|
||||
# -e "MINIO_SECRET_KEY=password" \
|
||||
# -v /data/media:/data \
|
||||
# minio/minio server --console-address :9001 /data
|
||||
- name: Start Minio
|
||||
run: |
|
||||
docker pull minio/minio
|
||||
docker run -d --name minio \
|
||||
-p 9000:9000 \
|
||||
-e "MINIO_ACCESS_KEY=impress" \
|
||||
-e "MINIO_SECRET_KEY=password" \
|
||||
-v /data/media:/data \
|
||||
minio/minio server --console-address :9001 /data
|
||||
|
||||
# - name: Configure MinIO
|
||||
# run: |
|
||||
# MINIO=$(docker ps | grep minio/minio | sed -E 's/.*\s+([a-zA-Z0-9_-]+)$/\1/')
|
||||
# docker exec ${MINIO} sh -c \
|
||||
# "mc alias set impress http://localhost:9000 impress password && \
|
||||
# mc alias ls && \
|
||||
# mc mb impress/impress-media-storage && \
|
||||
# mc version enable impress/impress-media-storage"
|
||||
- name: Configure MinIO
|
||||
run: |
|
||||
MINIO=$(docker ps | grep minio/minio | sed -E 's/.*\s+([a-zA-Z0-9_-]+)$/\1/')
|
||||
docker exec ${MINIO} sh -c \
|
||||
"mc alias set impress http://localhost:9000 impress password && \
|
||||
mc alias ls && \
|
||||
mc mb impress/impress-media-storage && \
|
||||
mc version enable impress/impress-media-storage"
|
||||
|
||||
# - name: Install Python
|
||||
# uses: actions/setup-python@v3
|
||||
# with:
|
||||
# python-version: "3.10"
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
# - name: Install development dependencies
|
||||
# run: pip install --user .[dev]
|
||||
- name: Install development dependencies
|
||||
run: pip install --user .[dev]
|
||||
|
||||
# - name: Install gettext (required to compile messages)
|
||||
# run: |
|
||||
# sudo apt-get update
|
||||
# sudo apt-get install -y gettext pandoc
|
||||
- name: Install gettext (required to compile messages)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gettext pandoc
|
||||
|
||||
# - name: Generate a MO file from strings extracted from the project
|
||||
# run: python manage.py compilemessages
|
||||
- name: Generate a MO file from strings extracted from the project
|
||||
run: python manage.py compilemessages
|
||||
|
||||
# - name: Run tests
|
||||
# run: ~/.local/bin/pytest -n 2
|
||||
- name: Run tests
|
||||
run: ~/.local/bin/pytest -n 2
|
||||
|
||||
@@ -39,3 +39,6 @@ LOGOUT_REDIRECT_URL=http://localhost:3000
|
||||
|
||||
OIDC_REDIRECT_ALLOWED_HOSTS=["http://localhost:8083", "http://localhost:3000"]
|
||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS={"acr_values": "eidas1"}
|
||||
|
||||
AI_BASE_URL=https://openaiendpoint.com
|
||||
AI_API_KEY=password
|
||||
@@ -16,8 +16,8 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ["id", "sub", "email"]
|
||||
read_only_fields = ["id", "sub", "email"]
|
||||
fields = ["id", "email"]
|
||||
read_only_fields = ["id", "email"]
|
||||
|
||||
|
||||
class BaseAccessSerializer(serializers.ModelSerializer):
|
||||
@@ -148,7 +148,6 @@ class DocumentSerializer(BaseResourceSerializer):
|
||||
"title",
|
||||
"accesses",
|
||||
"abilities",
|
||||
"is_e2ee",
|
||||
"link_role",
|
||||
"link_reach",
|
||||
"created_at",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""API endpoints"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
@@ -17,6 +18,9 @@ from django.db.models import (
|
||||
from django.http import Http404
|
||||
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
from rest_framework import (
|
||||
decorators,
|
||||
exceptions,
|
||||
@@ -367,6 +371,71 @@ class DocumentViewSet(
|
||||
pass
|
||||
|
||||
return drf_response.Response(serializer.data)
|
||||
|
||||
@decorators.action(detail=True, methods=["post"], url_path="ai")
|
||||
def ai(self, request, *args, **kwargs):
|
||||
"""
|
||||
Process text using AI based on the specified action.
|
||||
"""
|
||||
if not request.user.is_authenticated:
|
||||
raise exceptions.PermissionDenied("Authentication required.")
|
||||
|
||||
action = request.data.get("action")
|
||||
text = request.data.get("text")
|
||||
|
||||
client = OpenAI(
|
||||
base_url=settings.AI_BASE_URL,
|
||||
api_key=settings.AI_API_KEY
|
||||
)
|
||||
|
||||
action_configs = {
|
||||
"prompt": {
|
||||
"system_content": 'Answer to the prompt. The output should be in markdown format. Send the data back in this json format: {"answer_prompt": "Your markdown answer"}. Do not give any other information.',
|
||||
"response_key": 'answer_prompt'
|
||||
},
|
||||
"correct": {
|
||||
"system_content": 'You are a text corrector. Only correct the grammar and spelling of the given markdown text. Keep the language and markdown formatting in which the text was originally written. Return only a JSON in the following format: {"corrected_text": "your corrected markdown text"}. Do not provide any other information.',
|
||||
"response_key": 'corrected_text'
|
||||
},
|
||||
"rephrase": {
|
||||
"system_content": 'You are a writer. Rephrase the given markdown text. Keep the language and markdown formatting in which the text was originally written. Return only a JSON in the following format: {"rephrased_text": "your rephrased markdown text"}. Do not provide any other information.',
|
||||
"response_key": 'rephrased_text'
|
||||
},
|
||||
"summarize": {
|
||||
"system_content": 'You are a writer. Summarize the given markdown text. Keep the language in which the text was originally written and use appropriate markdown formatting. Return only a JSON in the following format: {"summary": "your markdown summary"}. Do not provide any other information.',
|
||||
"response_key": 'summary'
|
||||
},
|
||||
"translate_en": {
|
||||
"system_content": 'You are an English translator. Translate the given markdown text to English, preserving the markdown formatting. Return only a JSON in the following format: {"text": "Your translated markdown text in English"}. Do not provide any other information.',
|
||||
"response_key": 'text'
|
||||
},
|
||||
"translate_de": {
|
||||
"system_content": 'You are a German translator. Translate the given markdown text to German, preserving the markdown formatting. Return only a JSON in the following format: {"text": "Your translated markdown text in German"}. Do not provide any other information.',
|
||||
"response_key": 'text'
|
||||
},
|
||||
"translate_fr": {
|
||||
"system_content": 'You are a French translator. Translate the given markdown text to French, preserving the markdown formatting. Return only a JSON in the following format: {"text": "Your translated markdown text in French"}. Do not provide any other information.',
|
||||
"response_key": 'text'
|
||||
}
|
||||
}
|
||||
|
||||
if action not in action_configs:
|
||||
return drf_response.Response({"error": "Invalid action"}, status=400)
|
||||
|
||||
config = action_configs[action]
|
||||
try:
|
||||
response = client.chat.completions.create(
|
||||
model="meta-llama/Meta-Llama-3.1-70B-Instruct",
|
||||
response_format={ "type": "json_object"},
|
||||
messages=[
|
||||
{"role": "system", "content": config["system_content"]},
|
||||
{"role": "user", "content": json.dumps({"mardown_input": text})},
|
||||
]
|
||||
)
|
||||
corrected_response = json.loads(response.choices[0].message.content)
|
||||
return drf_response.Response(corrected_response[config["response_key"]])
|
||||
except (json.JSONDecodeError, KeyError) as e:
|
||||
return drf_response.Response({"error": f"Error processing AI response: {str(e)}"}, status=500)
|
||||
|
||||
@decorators.action(detail=True, methods=["get"], url_path="versions")
|
||||
def versions_list(self, request, *args, **kwargs):
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1 on 2024-09-12 13:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0005_remove_document_is_public_alter_document_link_reach_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='document',
|
||||
name='is_e2ee',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -329,7 +329,6 @@ class Document(BaseModel):
|
||||
link_role = models.CharField(
|
||||
max_length=20, choices=LinkRoleChoices.choices, default=LinkRoleChoices.READER
|
||||
)
|
||||
is_e2ee = models.BooleanField(default=False)
|
||||
|
||||
_content = None
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ def test_api_documents_retrieve_anonymous_public():
|
||||
"versions_retrieve": False,
|
||||
},
|
||||
"accesses": [],
|
||||
"is_e2ee": False,
|
||||
"link_reach": "public",
|
||||
"link_role": document.link_role,
|
||||
"title": document.title,
|
||||
@@ -88,7 +87,6 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
|
||||
"versions_retrieve": False,
|
||||
},
|
||||
"accesses": [],
|
||||
"is_e2ee": False,
|
||||
"link_reach": reach,
|
||||
"link_role": document.link_role,
|
||||
"title": document.title,
|
||||
@@ -196,7 +194,6 @@ def test_api_documents_retrieve_authenticated_related_direct():
|
||||
"title": document.title,
|
||||
"content": document.content,
|
||||
"abilities": document.get_abilities(user),
|
||||
"is_e2ee": False,
|
||||
"link_reach": document.link_reach,
|
||||
"link_role": document.link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
@@ -335,7 +332,6 @@ def test_api_documents_retrieve_authenticated_related_team_members(
|
||||
"title": document.title,
|
||||
"content": document.content,
|
||||
"abilities": document.get_abilities(user),
|
||||
"is_e2ee": False,
|
||||
"link_reach": "restricted",
|
||||
"link_role": document.link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
@@ -456,7 +452,6 @@ def test_api_documents_retrieve_authenticated_related_team_administrators(
|
||||
"title": document.title,
|
||||
"content": document.content,
|
||||
"abilities": document.get_abilities(user),
|
||||
"is_e2ee": False,
|
||||
"link_reach": "restricted",
|
||||
"link_role": document.link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
@@ -581,7 +576,6 @@ def test_api_documents_retrieve_authenticated_related_team_owners(
|
||||
"title": document.title,
|
||||
"content": document.content,
|
||||
"abilities": document.get_abilities(user),
|
||||
"is_e2ee": False,
|
||||
"link_reach": "restricted",
|
||||
"link_role": document.link_role,
|
||||
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
|
||||
|
||||
@@ -387,6 +387,12 @@ class Base(Configuration):
|
||||
ALLOW_LOGOUT_GET_METHOD = values.BooleanValue(
|
||||
default=True, environ_name="ALLOW_LOGOUT_GET_METHOD", environ_prefix=None
|
||||
)
|
||||
AI_BASE_URL = values.Value(
|
||||
None, environ_name="AI_BASE_URL", environ_prefix=None
|
||||
)
|
||||
AI_API_KEY = values.Value(
|
||||
None, environ_name="AI_API_KEY", environ_prefix=None
|
||||
)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@property
|
||||
|
||||
@@ -47,6 +47,7 @@ dependencies = [
|
||||
"jsonschema==4.23.0",
|
||||
"markdown==3.7",
|
||||
"nested-multipart-parser==1.5.0",
|
||||
"openai==1.44.1",
|
||||
"psycopg[binary]==3.2.1",
|
||||
"PyJWT==2.9.0",
|
||||
"pypandoc==1.13",
|
||||
|
||||
@@ -21,9 +21,6 @@
|
||||
"@gouvfr-lasuite/integration": "1.0.2",
|
||||
"@hocuspocus/provider": "2.13.5",
|
||||
"@openfun/cunningham-react": "2.9.4",
|
||||
"@socialgouv/e2esdk-client": "1.0.0-beta.28",
|
||||
"@socialgouv/e2esdk-devtools": "1.0.0-beta.38",
|
||||
"@socialgouv/e2esdk-react": "1.0.0-beta.28",
|
||||
"@tanstack/react-query": "5.55.4",
|
||||
"i18next": "23.15.1",
|
||||
"idb": "8.0.0",
|
||||
@@ -36,8 +33,8 @@
|
||||
"react-i18next": "15.0.1",
|
||||
"react-select": "5.8.0",
|
||||
"styled-components": "6.1.13",
|
||||
"y-protocols": "1.0.6",
|
||||
"yjs": "*",
|
||||
"y-protocols": "1.0.6",
|
||||
"zustand": "4.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { CunninghamProvider } from '@openfun/cunningham-react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { E2ESDKClientProvider } from '@socialgouv/e2esdk-react';
|
||||
import { e2esdkClient } from './auth/useAuthStore';
|
||||
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import '@/i18n/initI18n';
|
||||
@@ -29,9 +27,7 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CunninghamProvider theme={theme}>
|
||||
<E2ESDKClientProvider client={e2esdkClient}>
|
||||
<Auth>{children}</Auth>
|
||||
</E2ESDKClientProvider>
|
||||
<Auth>{children}</Auth>
|
||||
</CunninghamProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
* Represents user retrieved from the API.
|
||||
* @interface User
|
||||
* @property {string} id - The id of the user.
|
||||
* @property {string} sub - The `sub` field of OIDC
|
||||
* @property {string} email - The email of the user.
|
||||
* @property {string} name - The name of the user.
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
sub: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
@@ -5,31 +5,18 @@ import { baseApiUrl } from '@/core/conf';
|
||||
import { User, getMe } from './api';
|
||||
import { PATH_AUTH_LOCAL_STORAGE } from './conf';
|
||||
|
||||
import { Client, PublicUserIdentity } from '@socialgouv/e2esdk-client';
|
||||
import { identity } from 'lodash';
|
||||
|
||||
export const e2esdkClient = new Client({
|
||||
// Point it to where your server is listening
|
||||
serverURL: 'https://app-a5a1b445-32e0-4cf4-a478-821a48f86ccf.cleverapps.io',
|
||||
// Pass the signature public key you configured for the server
|
||||
serverSignaturePublicKey: 'ayfva9SUh0mfgmifUtxcdLp4HriHJiqefEKnvYgY4qM',
|
||||
});
|
||||
|
||||
interface AuthStore {
|
||||
initiated: boolean;
|
||||
authenticated: boolean;
|
||||
readyForEncryption: boolean;
|
||||
initAuth: () => void;
|
||||
logout: () => void;
|
||||
login: () => void;
|
||||
endToEndData?: PublicUserIdentity;
|
||||
userData?: User;
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
initiated: false,
|
||||
authenticated: false,
|
||||
readyForEncryption: false,
|
||||
userData: undefined,
|
||||
};
|
||||
|
||||
@@ -37,51 +24,22 @@ export const useAuthStore = create<AuthStore>((set) => ({
|
||||
initiated: initialState.initiated,
|
||||
authenticated: initialState.authenticated,
|
||||
userData: initialState.userData,
|
||||
readyForEncryption: initialState.readyForEncryption,
|
||||
|
||||
initAuth: () => {
|
||||
getMe()
|
||||
.then(
|
||||
(data: User) => {
|
||||
// If a path is stored in the local storage, we redirect to it
|
||||
const path_auth = localStorage.getItem(PATH_AUTH_LOCAL_STORAGE);
|
||||
if (path_auth) {
|
||||
localStorage.removeItem(PATH_AUTH_LOCAL_STORAGE);
|
||||
window.location.replace(path_auth);
|
||||
return;
|
||||
}
|
||||
.then((data: User) => {
|
||||
// If a path is stored in the local storage, we redirect to it
|
||||
const path_auth = localStorage.getItem(PATH_AUTH_LOCAL_STORAGE);
|
||||
if (path_auth) {
|
||||
localStorage.removeItem(PATH_AUTH_LOCAL_STORAGE);
|
||||
window.location.replace(path_auth);
|
||||
return;
|
||||
}
|
||||
|
||||
set({ authenticated: true, userData: data });
|
||||
return e2esdkClient
|
||||
.signup(data.sub)
|
||||
.then(() => data)
|
||||
.catch(() => data);
|
||||
},
|
||||
() => {},
|
||||
)
|
||||
.then(
|
||||
(data) => {
|
||||
set({ readyForEncryption: true });
|
||||
if (data) {
|
||||
return e2esdkClient.login(data.sub);
|
||||
}
|
||||
},
|
||||
(e) => {
|
||||
throw e;
|
||||
//if (data) {
|
||||
// return e2esdkClient.login(data.sub);
|
||||
//}
|
||||
//fail
|
||||
},
|
||||
)
|
||||
.then((publicIdentity: PublicUserIdentity | null | undefined) => {
|
||||
if (!publicIdentity) throw Error('exploding');
|
||||
console.log('publicIdentity', publicIdentity);
|
||||
set({ endToEndData: publicIdentity });
|
||||
set({ authenticated: true, userData: data });
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
console.log('finally');
|
||||
set({ initiated: true });
|
||||
});
|
||||
},
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
|
||||
export type AIActions =
|
||||
| 'prompt'
|
||||
| 'rephrase'
|
||||
| 'summarize'
|
||||
| 'translate'
|
||||
| 'correct'
|
||||
| 'translate_fr'
|
||||
| 'translate_en'
|
||||
| 'translate_de';
|
||||
|
||||
export type DocAIParams = {
|
||||
docId: string;
|
||||
text: string;
|
||||
action: AIActions;
|
||||
};
|
||||
|
||||
export type DocAIResponse = string;
|
||||
|
||||
export const DocAI = async ({
|
||||
docId,
|
||||
...params
|
||||
}: DocAIParams): Promise<DocAIResponse> => {
|
||||
const response = await fetchAPI(`documents/${docId}/ai/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
...params,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError('Failed to get AI', await errorCauses(response));
|
||||
}
|
||||
|
||||
return response.json() as Promise<DocAIResponse>;
|
||||
};
|
||||
|
||||
export function useAIRewrite() {
|
||||
return useMutation<DocAIResponse, APIError, DocAIParams>({
|
||||
mutationFn: DocAI,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import {
|
||||
useBlockNoteEditor,
|
||||
useComponentsContext,
|
||||
useSelectedBlocks,
|
||||
ComponentProps,
|
||||
} from '@blocknote/react';
|
||||
import { mergeRefs } from "@mantine/hooks";
|
||||
|
||||
import { ReactNode, useMemo, forwardRef, useRef, useCallback, useState, createContext } from 'react';
|
||||
|
||||
import {
|
||||
Menu as MantineMenu,
|
||||
} from "@mantine/core";
|
||||
|
||||
import {Box, Text } from '@/components';
|
||||
|
||||
import { Doc } from '../../doc-management';
|
||||
import { AIActions, useAIRewrite } from '../api/useAIRewrite';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface AIGroupButtonProps {
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
export function AIGroupButton({ doc }: AIGroupButtonProps) {
|
||||
const editor = useBlockNoteEditor();
|
||||
const Components = useComponentsContext();
|
||||
const selectedBlocks = useSelectedBlocks(editor);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const show = useMemo(() => {
|
||||
return !!selectedBlocks.find((block) => block.content !== undefined);
|
||||
}, [selectedBlocks]);
|
||||
|
||||
if (!show || !editor.isEditable || !Components) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Components.Generic.Menu.Root>
|
||||
<Components.Generic.Menu.Trigger>
|
||||
<Components.FormattingToolbar.Button
|
||||
className="bn-button"
|
||||
data-test="ai-actions"
|
||||
label="AI"
|
||||
mainTooltip={t('AI Actions')}
|
||||
>
|
||||
AI
|
||||
</Components.FormattingToolbar.Button>
|
||||
</Components.Generic.Menu.Trigger>
|
||||
<Components.Generic.Menu.Dropdown className="bn-menu-dropdown bn-drag-handle-menu">
|
||||
<AIMenuItem action="prompt" docId={doc.id} icon={<Text $isMaterialIcon $size="s">text_fields</Text>}>
|
||||
{t('Use as prompt')}
|
||||
</AIMenuItem>
|
||||
<AIMenuItem action="rephrase" docId={doc.id} icon={<Text $isMaterialIcon $size="s">refresh</Text>}>
|
||||
{t('Rephrase')}
|
||||
</AIMenuItem>
|
||||
<AIMenuItem action="summarize" docId={doc.id} icon={<Text $isMaterialIcon $size="s">summarize</Text>}>
|
||||
{t('Summarize')}
|
||||
</AIMenuItem>
|
||||
<AIMenuItem action="correct" docId={doc.id} icon={<Text $isMaterialIcon $size="s">check</Text>}>
|
||||
{t('Correct')}
|
||||
</AIMenuItem>
|
||||
<TranslateMenu docId={doc.id} />
|
||||
</Components.Generic.Menu.Dropdown>
|
||||
</Components.Generic.Menu.Root>
|
||||
);
|
||||
}
|
||||
|
||||
interface AIMenuItemProps {
|
||||
action: AIActions;
|
||||
docId: Doc['id'];
|
||||
children: ReactNode;
|
||||
icon: ReactNode;
|
||||
}
|
||||
|
||||
const AIMenuItem = ({ action, docId, children, icon }: AIMenuItemProps) => {
|
||||
const editor = useBlockNoteEditor();
|
||||
const Components = useComponentsContext()!;
|
||||
const { mutateAsync: requestAI, isPending } = useAIRewrite();
|
||||
|
||||
const handleAIAction = useCallback(async () => {
|
||||
const selectedBlocks = editor.getSelection()?.blocks;
|
||||
|
||||
if (!selectedBlocks || selectedBlocks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const markdown = await editor.blocksToMarkdownLossy(selectedBlocks);
|
||||
const newText = await requestAI({
|
||||
docId,
|
||||
text: markdown,
|
||||
action,
|
||||
});
|
||||
|
||||
const blockMarkdown = await editor.tryParseMarkdownToBlocks(newText);
|
||||
editor.replaceBlocks(selectedBlocks, blockMarkdown);
|
||||
}, [editor, requestAI, docId, action]);
|
||||
|
||||
return (
|
||||
<Components.Generic.Menu.Item
|
||||
closeMenuOnClick={false}
|
||||
icon={icon}
|
||||
onClick={handleAIAction}
|
||||
rightSection={isPending && <Box className="loader" />}
|
||||
>
|
||||
{children}
|
||||
</Components.Generic.Menu.Item>
|
||||
);
|
||||
};
|
||||
|
||||
interface TranslateMenuProps {
|
||||
docId: Doc['id'];
|
||||
}
|
||||
|
||||
const TranslateMenu = ({ docId }: TranslateMenuProps) => {
|
||||
const Components = useComponentsContext()!;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SubMenu position="right" sub={true} icon={<Text $isMaterialIcon $size="s">translate</Text>} close>
|
||||
<Components.Generic.Menu.Trigger sub={true}>
|
||||
<Components.Generic.Menu.Item subTrigger={true}>
|
||||
{t('Translate')}
|
||||
</Components.Generic.Menu.Item>
|
||||
</Components.Generic.Menu.Trigger>
|
||||
<Components.Generic.Menu.Dropdown className="bn-menu-dropdown bn-color-picker-dropdown">
|
||||
<AIMenuItem action="translate_en" docId={docId}>
|
||||
{t('English')}
|
||||
</AIMenuItem>
|
||||
<AIMenuItem action="translate_fr" docId={docId}>
|
||||
{t('French')}
|
||||
</AIMenuItem>
|
||||
<AIMenuItem action="translate_de" docId={docId}>
|
||||
{t('German')}
|
||||
</AIMenuItem>
|
||||
</Components.Generic.Menu.Dropdown>
|
||||
</SubMenu>
|
||||
);
|
||||
};
|
||||
|
||||
const SubMenuContext = createContext<
|
||||
| {
|
||||
onMenuMouseOver: () => void;
|
||||
onMenuMouseLeave: () => void;
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
||||
|
||||
const SubMenu = forwardRef<
|
||||
HTMLButtonElement,
|
||||
ComponentProps["Generic"]["Menu"]["Root"]
|
||||
>((props, ref) => {
|
||||
const {
|
||||
children,
|
||||
onOpenChange,
|
||||
position,
|
||||
icon,
|
||||
sub, // not used
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const [opened, setOpened] = useState(false);
|
||||
|
||||
const itemRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
const menuCloseTimer = useRef<ReturnType<typeof setTimeout> | undefined>();
|
||||
|
||||
const mouseLeave = useCallback(() => {
|
||||
if (menuCloseTimer.current) {
|
||||
clearTimeout(menuCloseTimer.current);
|
||||
}
|
||||
menuCloseTimer.current = setTimeout(() => {
|
||||
setOpened(false);
|
||||
}, 250);
|
||||
}, []);
|
||||
|
||||
const mouseOver = useCallback(() => {
|
||||
if (menuCloseTimer.current) {
|
||||
clearTimeout(menuCloseTimer.current);
|
||||
}
|
||||
setOpened(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SubMenuContext.Provider
|
||||
value={{
|
||||
onMenuMouseOver: mouseOver,
|
||||
onMenuMouseLeave: mouseLeave,
|
||||
}}>
|
||||
<MantineMenu.Item
|
||||
className="bn-menu-item bn-mt-sub-menu-item"
|
||||
closeMenuOnClick={false}
|
||||
ref={mergeRefs(ref, itemRef)}
|
||||
onMouseOver={mouseOver}
|
||||
onMouseLeave={mouseLeave}
|
||||
leftSection={icon}>
|
||||
<MantineMenu
|
||||
portalProps={{
|
||||
target: itemRef.current
|
||||
? itemRef.current.parentElement!
|
||||
: undefined,
|
||||
}}
|
||||
middlewares={{ flip: true, shift: true, inline: false, size: true }}
|
||||
trigger={"hover"}
|
||||
opened={opened}
|
||||
onClose={() => onOpenChange?.(false)}
|
||||
onOpen={() => onOpenChange?.(true)}
|
||||
position={position}>
|
||||
{children}
|
||||
</MantineMenu>
|
||||
</MantineMenu.Item>
|
||||
</SubMenuContext.Provider>
|
||||
);
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Block, BlockNoteEditor as BlockNoteEditorCore, PartialBlock } from '@blocknote/core';
|
||||
import { BlockNoteEditor as BlockNoteEditorCore } from '@blocknote/core';
|
||||
import '@blocknote/core/fonts/inter.css';
|
||||
import { BlockNoteView } from '@blocknote/mantine';
|
||||
import '@blocknote/mantine/style.css';
|
||||
@@ -17,7 +17,6 @@ import { useDocStore } from '../stores';
|
||||
import { randomColor } from '../utils';
|
||||
|
||||
import { BlockNoteToolbar } from './BlockNoteToolbar';
|
||||
import { useE2ESDKClient } from '@socialgouv/e2esdk-react';
|
||||
|
||||
const cssEditor = `
|
||||
&, & > .bn-container, & .ProseMirror {
|
||||
@@ -72,8 +71,7 @@ export const BlockNoteContent = ({
|
||||
const { userData } = useAuthStore();
|
||||
const { setStore, docsStore } = useDocStore();
|
||||
const canSave = doc.abilities.partial_update && !isVersion;
|
||||
|
||||
const e2eClient = useE2ESDKClient();
|
||||
useSaveDoc(doc.id, provider.document, canSave);
|
||||
const storedEditor = docsStore?.[storeId]?.editor;
|
||||
const {
|
||||
mutateAsync: createDocAttachment,
|
||||
@@ -101,46 +99,18 @@ export const BlockNoteContent = ({
|
||||
return storedEditor;
|
||||
}
|
||||
|
||||
// TODO decrypt doc.content
|
||||
//localStorage.getItem('KEY');
|
||||
|
||||
const docId = doc.id;
|
||||
const purpose = `docs:${docId}`;
|
||||
const key = e2eClient.findKeyByPurpose(purpose);
|
||||
console.log('purpose', purpose, 'key', key);
|
||||
let plaintextContent: Array<PartialBlock> | undefined;
|
||||
if (!key) {
|
||||
alert('probleme de key');
|
||||
return;
|
||||
} else {
|
||||
if (doc.content) {
|
||||
plaintextContent = JSON.parse(e2eClient.decrypt(
|
||||
doc.content,
|
||||
key.keychainFingerprint,
|
||||
) as string) as Array<PartialBlock>;
|
||||
|
||||
console.log('decryptedMessage', plaintextContent);
|
||||
} else {
|
||||
plaintextContent = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return BlockNoteEditorCore.create({
|
||||
// collaboration: {
|
||||
// provider,
|
||||
// fragment: provider.document.getXmlFragment('document-store'),
|
||||
// user: {
|
||||
// name: userData?.email || 'Anonymous',
|
||||
// color: randomColor(),
|
||||
// },
|
||||
// },
|
||||
collaboration: {
|
||||
provider,
|
||||
fragment: provider.document.getXmlFragment('document-store'),
|
||||
user: {
|
||||
name: userData?.email || 'Anonymous',
|
||||
color: randomColor(),
|
||||
},
|
||||
},
|
||||
uploadFile,
|
||||
initialContent: plaintextContent,
|
||||
});
|
||||
}, [doc.content, storedEditor, uploadFile]);
|
||||
|
||||
console.log("useSaveDoc", doc.id, provider.document, canSave, editor);
|
||||
useSaveDoc(doc.id, provider.document, canSave, editor);
|
||||
}, [provider, storedEditor, uploadFile, userData?.email]);
|
||||
|
||||
useEffect(() => {
|
||||
setStore(storeId, { editor });
|
||||
@@ -160,7 +130,7 @@ export const BlockNoteContent = ({
|
||||
editable={doc.abilities.partial_update && !isVersion}
|
||||
theme="light"
|
||||
>
|
||||
<BlockNoteToolbar />
|
||||
<BlockNoteToolbar doc={doc} />
|
||||
</BlockNoteView>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -16,13 +16,25 @@ import {
|
||||
import { forEach, isArray } from 'lodash';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
export const BlockNoteToolbar = () => {
|
||||
import { Doc } from '../../doc-management';
|
||||
|
||||
// import { AIButton } from './AIButton';
|
||||
import { AIGroupButton } from './AIButton';
|
||||
|
||||
interface BlockNoteToolbarProps {
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
export const BlockNoteToolbar = ({ doc }: BlockNoteToolbarProps) => {
|
||||
return (
|
||||
<FormattingToolbarController
|
||||
formattingToolbar={() => (
|
||||
<FormattingToolbar>
|
||||
<BlockTypeSelect key="blockTypeSelect" />
|
||||
|
||||
{/* Extra button to convert from markdown to json */}
|
||||
<AIGroupButton key="AIButton" doc={doc} />
|
||||
|
||||
{/* Extra button to convert from markdown to json */}
|
||||
<MarkdownButton key="customButton" />
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { BlockNoteEditor } from '@blocknote/core';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import * as Y from 'yjs';
|
||||
@@ -7,24 +6,17 @@ import { useUpdateDoc } from '@/features/docs/doc-management/';
|
||||
import { KEY_LIST_DOC_VERSIONS } from '@/features/docs/doc-versioning';
|
||||
|
||||
import { toBase64 } from '../utils';
|
||||
import { useE2ESDKClient } from '@socialgouv/e2esdk-react';
|
||||
|
||||
const useSaveDoc = (
|
||||
docId: string,
|
||||
doc: Y.Doc,
|
||||
canSave: boolean,
|
||||
editor: BlockNoteEditor,
|
||||
) => {
|
||||
const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => {
|
||||
const { mutate: updateDoc } = useUpdateDoc({
|
||||
listInvalideQueries: [KEY_LIST_DOC_VERSIONS],
|
||||
});
|
||||
const e2eClient = useE2ESDKClient();
|
||||
const [initialDoc, setInitialDoc] = useState<string>(
|
||||
JSON.stringify(editor.document)
|
||||
toBase64(Y.encodeStateAsUpdate(doc)),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setInitialDoc(JSON.stringify(editor.document));
|
||||
setInitialDoc(toBase64(Y.encodeStateAsUpdate(doc)));
|
||||
}, [doc]);
|
||||
|
||||
/**
|
||||
@@ -40,7 +32,7 @@ const useSaveDoc = (
|
||||
transaction: Y.Transaction,
|
||||
) => {
|
||||
if (!transaction.local) {
|
||||
setInitialDoc(JSON.stringify(editor.document));
|
||||
setInitialDoc(toBase64(Y.encodeStateAsUpdate(updatedDoc)));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -55,41 +47,23 @@ const useSaveDoc = (
|
||||
* Check if the doc has been updated and can be saved.
|
||||
*/
|
||||
const hasChanged = useCallback(() => {
|
||||
return initialDoc !== JSON.stringify(editor.document);
|
||||
const newDoc = toBase64(Y.encodeStateAsUpdate(doc));
|
||||
return initialDoc !== newDoc;
|
||||
}, [doc, initialDoc]);
|
||||
|
||||
const shouldSave = useCallback(() => {
|
||||
console.log('hasChanged', hasChanged(), 'canSave', canSave);
|
||||
return hasChanged() && canSave;
|
||||
}, [canSave, hasChanged]);
|
||||
|
||||
const saveDoc = useCallback(() => {
|
||||
const newDoc = JSON.stringify(editor.document);
|
||||
//const newDoc = toBase64(Y.encodeStateAsUpdate(doc));
|
||||
|
||||
// TODO encode the content
|
||||
|
||||
const purpose = `docs:${docId}`;
|
||||
const key = e2eClient.findKeyByPurpose(purpose);
|
||||
console.log("purpose", purpose, "key", key);
|
||||
if (!key) {
|
||||
alert('probleme de key');
|
||||
return;
|
||||
}
|
||||
|
||||
const encrypted = e2eClient.encrypt(newDoc, key.keychainFingerprint);
|
||||
|
||||
console.log('encrypted', encrypted);
|
||||
|
||||
// todo
|
||||
|
||||
const newDoc = toBase64(Y.encodeStateAsUpdate(doc));
|
||||
setInitialDoc(newDoc);
|
||||
|
||||
updateDoc({
|
||||
id: docId,
|
||||
content: encrypted,
|
||||
content: newDoc,
|
||||
});
|
||||
}, [docId, editor?.document, updateDoc]);
|
||||
}, [doc, docId, updateDoc]);
|
||||
|
||||
const timeout = useRef<NodeJS.Timeout>();
|
||||
const router = useRouter();
|
||||
@@ -100,12 +74,10 @@ const useSaveDoc = (
|
||||
}
|
||||
|
||||
const onSave = (e?: Event) => {
|
||||
console.log('entered onSave');
|
||||
if (!shouldSave()) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('will save');
|
||||
saveDoc();
|
||||
|
||||
/**
|
||||
@@ -124,7 +96,7 @@ const useSaveDoc = (
|
||||
};
|
||||
|
||||
// Save every minute
|
||||
timeout.current = setInterval(onSave, 1000);
|
||||
timeout.current = setInterval(onSave, 60000);
|
||||
// Save when the user leaves the page
|
||||
addEventListener('beforeunload', onSave);
|
||||
// Save when the user navigates to another page
|
||||
|
||||
@@ -26,9 +26,9 @@ export const useDocStore = create<UseDocStore>((set, get) => ({
|
||||
guid: storeId,
|
||||
});
|
||||
|
||||
// if (initialDoc) {
|
||||
// Y.applyUpdate(doc, Buffer.from(initialDoc, 'base64'));
|
||||
// }
|
||||
if (initialDoc) {
|
||||
Y.applyUpdate(doc, Buffer.from(initialDoc, 'base64'));
|
||||
}
|
||||
|
||||
const provider = new HocuspocusProvider({
|
||||
url: providerUrl(storeId),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { e2esdkClient } from '@/core/auth/useAuthStore';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
|
||||
@@ -14,7 +13,6 @@ export const createDoc = async ({ title }: CreateDocParam): Promise<Doc> => {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
is_e2ee: true
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -22,16 +20,7 @@ export const createDoc = async ({ title }: CreateDocParam): Promise<Doc> => {
|
||||
throw new APIError('Failed to create the doc', await errorCauses(response));
|
||||
}
|
||||
|
||||
const resp = await (response.json() as Promise<Doc>);
|
||||
|
||||
const { keychainFingerprint } = await e2esdkClient.createNewKeychain(
|
||||
`docs:${resp.id}`,
|
||||
'secretBox'
|
||||
);
|
||||
|
||||
console.log('new e2ee keychain registered', keychainFingerprint);
|
||||
|
||||
return resp;
|
||||
return response.json() as Promise<Doc>;
|
||||
};
|
||||
|
||||
interface CreateDocProps {
|
||||
|
||||
@@ -37,7 +37,6 @@ export interface Doc {
|
||||
accesses: Access[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
is_e2ee: boolean;
|
||||
abilities: {
|
||||
destroy: boolean;
|
||||
link_configuration: boolean;
|
||||
|
||||
@@ -140,7 +140,16 @@
|
||||
"accessibility-dinum-services": "<strong>DINUM</strong> s'engage à rendre accessibles ses services numériques, conformément à l'article 47 de la loi n° 2005-102 du 11 février 2005.",
|
||||
"accessibility-form-defenseurdesdroits": "Écrire un message au<1>Défenseur des droits</1>",
|
||||
"accessibility-not-audit": "<strong>docs.numerique.gouv.fr</strong> n'est pas en conformité avec le RGAA 4.1. Le site n'a <strong>pas encore été audité.</strong>",
|
||||
"you have reported to the website manager a lack of accessibility that prevents you from accessing content or one of the services of the portal and you have not received a satisfactory response.": "vous avez signalé au responsable du site internet un défaut d'accessibilité qui vous empêche d'accéder à un contenu ou à un des services du portail et vous n'avez pas obtenu de réponse satisfaisante."
|
||||
"you have reported to the website manager a lack of accessibility that prevents you from accessing content or one of the services of the portal and you have not received a satisfactory response.": "vous avez signalé au responsable du site internet un défaut d'accessibilité qui vous empêche d'accéder à un contenu ou à un des services du portail et vous n'avez pas obtenu de réponse satisfaisante.",
|
||||
"AI Actions": "Actions IA",
|
||||
"Use as prompt": "Utiliser comme prompt",
|
||||
"Rephrase": "Reformuler",
|
||||
"Summarize": "Résumer",
|
||||
"Correct": "Corriger",
|
||||
"Translate": "Traduire",
|
||||
"English": "Anglais",
|
||||
"French": "Français",
|
||||
"German": "Allemand"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,3 +41,17 @@ main ::-webkit-scrollbar-thumb:hover,
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
}
|
||||
|
||||
.loader {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #3498db;
|
||||
border-radius: 71%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
25141
src/frontend/yarn.lock
25141
src/frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user