Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ee9f9152c | |||
| b7914dfeef | |||
| 1b2f74660b | |||
| 25491aeb06 | |||
| 6bb56e6b72 | |||
| cdf157c72b | |||
| e26fcdf985 | |||
| 5cc4b07cf6 | |||
| 0cfc242e09 | |||
| a6b3cfdb0c | |||
| 5ead18c94c | |||
| 5eeb8cae5c | |||
| 68bf024005 | |||
| fdd1068c90 | |||
| ba695bf647 | |||
| 27e7aec193 | |||
| 58b712a1de | |||
| 08f9036523 |
+5
-1
@@ -13,11 +13,15 @@ and this project adheres to
|
||||
|
||||
- 📝(doc) Add security.md and codeofconduct.md #604
|
||||
- ✨(frontend) add home page #553
|
||||
|
||||
- ✨(frontend) cursor display on activity #609
|
||||
- ✨(frontend) Add export page break #623
|
||||
|
||||
## Fixed
|
||||
|
||||
🌐(CI) Fix email partially translated #616
|
||||
- 🐛(frontend) fix cursor breakline #609
|
||||
- 🐛(frontend) fix style pdf export #609
|
||||
|
||||
|
||||
## [2.1.0] - 2025-01-29
|
||||
|
||||
|
||||
+12
-11
@@ -21,8 +21,8 @@ services:
|
||||
- MINIO_ROOT_USER=impress
|
||||
- MINIO_ROOT_PASSWORD=password
|
||||
ports:
|
||||
- '9000:9000'
|
||||
- '9001:9001'
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
entrypoint: ""
|
||||
command: minio server --console-address :9001 /data
|
||||
volumes:
|
||||
@@ -59,11 +59,11 @@ services:
|
||||
- ./src/backend:/app
|
||||
- ./data/static:/data/static
|
||||
depends_on:
|
||||
- postgresql
|
||||
- mailcatcher
|
||||
- redis
|
||||
- createbuckets
|
||||
|
||||
- postgresql
|
||||
- mailcatcher
|
||||
- redis
|
||||
- createbuckets
|
||||
|
||||
celery-dev:
|
||||
user: ${DOCKER_USER:-1000}
|
||||
image: impress:backend-development
|
||||
@@ -122,7 +122,7 @@ services:
|
||||
|
||||
frontend-dev:
|
||||
user: "${DOCKER_USER:-1000}"
|
||||
build:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./src/frontend/Dockerfile
|
||||
target: frontend-production
|
||||
@@ -151,13 +151,13 @@ services:
|
||||
image: node:18
|
||||
user: "${DOCKER_USER:-1000}"
|
||||
environment:
|
||||
HOME: /tmp
|
||||
HOME: /tmp
|
||||
volumes:
|
||||
- ".:/app"
|
||||
|
||||
y-provider:
|
||||
user: ${DOCKER_USER:-1000}
|
||||
build:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./src/frontend/servers/y-provider/Dockerfile
|
||||
target: y-provider
|
||||
@@ -169,6 +169,7 @@ services:
|
||||
|
||||
kc_postgresql:
|
||||
image: postgres:14.3
|
||||
platform: linux/amd64
|
||||
ports:
|
||||
- "5433:5432"
|
||||
env_file:
|
||||
@@ -196,7 +197,7 @@ services:
|
||||
KC_DB_PASSWORD: pass
|
||||
KC_DB_USERNAME: impress
|
||||
KC_DB_SCHEMA: public
|
||||
PROXY_ADDRESS_FORWARDING: 'true'
|
||||
PROXY_ADDRESS_FORWARDING: "true"
|
||||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
|
||||
@@ -10,6 +10,10 @@ LOGGING_LEVEL_HANDLERS_CONSOLE=INFO
|
||||
LOGGING_LEVEL_LOGGERS_ROOT=INFO
|
||||
LOGGING_LEVEL_LOGGERS_APP=INFO
|
||||
|
||||
# Y-Provider
|
||||
Y_PROVIDER_API_KEY="yprovider-api-key"
|
||||
Y_PROVIDER_API_BASE_URL=http://y-provider:4444/api/
|
||||
|
||||
# Python
|
||||
PYTHONPATH=/app
|
||||
|
||||
@@ -54,6 +58,9 @@ AI_BASE_URL=https://openaiendpoint.com
|
||||
AI_API_KEY=password
|
||||
AI_MODEL=llama
|
||||
|
||||
# Accessibility API
|
||||
ACCESSIBILITY_API_BASE_URL=https://localhost:8000
|
||||
|
||||
# Collaboration
|
||||
COLLABORATION_API_URL=http://nginx:8083/collaboration/api/
|
||||
COLLABORATION_SERVER_ORIGIN=http://localhost:3000
|
||||
|
||||
@@ -16,6 +16,7 @@ from core.services.converter_services import (
|
||||
ConversionError,
|
||||
YdocConverter,
|
||||
)
|
||||
from core.services.ai_services import AIService
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
@@ -306,7 +307,7 @@ class ServerCreateDocumentSerializer(serializers.Serializer):
|
||||
if user:
|
||||
email = user.email
|
||||
language = user.language or language
|
||||
|
||||
|
||||
try:
|
||||
document_content = YdocConverter().convert_markdown(
|
||||
validated_data["content"]
|
||||
@@ -568,6 +569,56 @@ class AITranslateSerializer(serializers.Serializer):
|
||||
if len(value.strip()) == 0:
|
||||
raise serializers.ValidationError("Text field cannot be empty.")
|
||||
return value
|
||||
|
||||
|
||||
class AIPdfTranscribeSerializer(serializers.Serializer):
|
||||
"""Serializer for AI PDF transcribe requests."""
|
||||
|
||||
pdfUrl = serializers.CharField(required=True)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize with user."""
|
||||
self.user = kwargs.pop('user', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def validate_pdfUrl(self, value):
|
||||
"""Ensure the pdfUrl field is a valid URL."""
|
||||
if not value.startswith(settings.MEDIA_BASE_URL):
|
||||
raise serializers.ValidationError("Invalid PDF URL format.")
|
||||
return value
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Create a new document for the transcribed content."""
|
||||
if not self.user:
|
||||
raise serializers.ValidationError("User is required")
|
||||
|
||||
# Get the transcribed content from AI service
|
||||
pdf_url = validated_data["pdfUrl"]
|
||||
response = AIService().transcribe_pdf(pdf_url)
|
||||
|
||||
try:
|
||||
# Convert the markdown content to YDoc format
|
||||
document_content = YdocConverter().convert_markdown(response)
|
||||
except ConversionError as err:
|
||||
raise serializers.ValidationError(
|
||||
{"content": [f"Could not convert transcribed content: {str(err)}"]}
|
||||
) from err
|
||||
|
||||
# Create the document as root node with converted content
|
||||
document = models.Document.add_root(
|
||||
title="PDF Transcription",
|
||||
content=document_content,
|
||||
creator=self.user,
|
||||
)
|
||||
|
||||
# Create owner access for the user
|
||||
models.DocumentAccess.objects.create(
|
||||
document=document,
|
||||
role=models.RoleChoices.OWNER,
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
return document
|
||||
|
||||
|
||||
class MoveDocumentSerializer(serializers.Serializer):
|
||||
|
||||
@@ -1079,6 +1079,41 @@ class DocumentViewSet(
|
||||
|
||||
return drf.response.Response(response, status=drf.status.HTTP_200_OK)
|
||||
|
||||
@drf.decorators.action(
|
||||
detail=True,
|
||||
methods=["post"],
|
||||
name="Just proxy ai call",
|
||||
url_path="ai-proxy"
|
||||
)
|
||||
def ai_proxy(self, request, *args, **kwargs):
|
||||
"""
|
||||
POST /api/v1.0/documents/<resource_id>/ai-transform
|
||||
with expected data:
|
||||
- text: str
|
||||
- action: str [prompt, correct, rephrase, summarize]
|
||||
Return JSON response with the processed text.
|
||||
"""
|
||||
print('PROXY 1')
|
||||
# Check permissions first
|
||||
# self.get_object()
|
||||
|
||||
print('PROXY 2')
|
||||
print(request.data)
|
||||
# serializer = serializers.AITransformSerializer(data=request.data)
|
||||
# serializer.is_valid(raise_exception=True)
|
||||
|
||||
print('PROXY 3')
|
||||
system_content = request.data["system"]
|
||||
text = request.data["text"]
|
||||
|
||||
print('PROXY 4')
|
||||
response = AIService().call_proxy(system_content, text)
|
||||
|
||||
print('PROXY 5')
|
||||
print(response)
|
||||
|
||||
return drf.response.Response(response, status=drf.status.HTTP_200_OK)
|
||||
|
||||
@drf.decorators.action(
|
||||
detail=True,
|
||||
methods=["post"],
|
||||
@@ -1107,6 +1142,32 @@ class DocumentViewSet(
|
||||
|
||||
return drf.response.Response(response, status=drf.status.HTTP_200_OK)
|
||||
|
||||
@drf.decorators.action(
|
||||
detail=True,
|
||||
methods=["post"],
|
||||
name="Transcribe PDF with AI",
|
||||
url_path="ai-pdf-transcribe",
|
||||
throttle_classes=[utils.AIDocumentRateThrottle, utils.AIUserRateThrottle],
|
||||
)
|
||||
def ai_pdf_transcribe(self, request, *args, **kwargs):
|
||||
"""
|
||||
POST /api/v1.0/documents/<resource_id>/ai-pdf-transcribe
|
||||
with expected data:
|
||||
- pdfUrl: str
|
||||
Return JSON response with the new document ID containing the transcription.
|
||||
"""
|
||||
serializer = serializers.AIPdfTranscribeSerializer(
|
||||
data=request.data,
|
||||
user=request.user
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
document = serializer.save()
|
||||
|
||||
return drf.response.Response(
|
||||
{"document_id": str(document.id)},
|
||||
status=drf.status.HTTP_201_CREATED
|
||||
)
|
||||
|
||||
|
||||
class DocumentAccessViewSet(
|
||||
ResourceAccessViewsetMixin,
|
||||
|
||||
@@ -2,13 +2,18 @@
|
||||
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
import requests
|
||||
import botocore
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.files.storage import default_storage
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
from core import enums
|
||||
from core.models import Document
|
||||
|
||||
AI_ACTIONS = {
|
||||
"prompt": (
|
||||
@@ -55,6 +60,22 @@ class AIService:
|
||||
raise ImproperlyConfigured("AI configuration not set")
|
||||
self.client = OpenAI(base_url=settings.AI_BASE_URL, api_key=settings.AI_API_KEY)
|
||||
|
||||
def call_proxy(self, system_content, text):
|
||||
messages = [
|
||||
{"role": "system", "content": system_content},
|
||||
{"role": "user", "content": text},
|
||||
]
|
||||
print('REQUEST', messages)
|
||||
response = self.client.chat.completions.create(
|
||||
model=settings.AI_MODEL,
|
||||
messages=messages,
|
||||
)
|
||||
|
||||
print('RESPONSE', response)
|
||||
content = response.choices[0].message.content
|
||||
print('CONTENT', content)
|
||||
return content
|
||||
|
||||
def call_ai_api(self, system_content, text):
|
||||
"""Helper method to call the OpenAI API and process the response."""
|
||||
response = self.client.chat.completions.create(
|
||||
@@ -96,3 +117,26 @@ class AIService:
|
||||
language_display = enums.ALL_LANGUAGES.get(language, language)
|
||||
system_content = AI_TRANSLATE.format(language=language_display)
|
||||
return self.call_ai_api(system_content, text)
|
||||
|
||||
def transcribe_pdf(self, pdf_url):
|
||||
"""Transcribe PDF using the accessibility hackathon API and create a new document."""
|
||||
try:
|
||||
media_prefix = os.path.join(settings.MEDIA_BASE_URL, "media")
|
||||
key = pdf_url[len(media_prefix):]
|
||||
|
||||
pdf_response = default_storage.connection.meta.client.get_object(
|
||||
Bucket=default_storage.bucket_name,
|
||||
Key=key
|
||||
)
|
||||
pdf_content = pdf_response['Body'].read()
|
||||
|
||||
api_url = f"{settings.ACCESSIBILITY_API_BASE_URL}/transcribe/pdf"
|
||||
files = {'file': ('document.pdf', pdf_content, 'application/pdf')}
|
||||
headers = {'Accept': 'application/json'}
|
||||
response = requests.post(api_url, files=files, headers=headers)
|
||||
response.raise_for_status()
|
||||
|
||||
transcribed_text = response.json()['markdown_content']
|
||||
return transcribed_text
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to transcribe PDF: {str(e)}")
|
||||
|
||||
@@ -517,6 +517,12 @@ class Base(Configuration):
|
||||
AI_BASE_URL = values.Value(None, environ_name="AI_BASE_URL", environ_prefix=None)
|
||||
AI_MODEL = values.Value(None, environ_name="AI_MODEL", environ_prefix=None)
|
||||
|
||||
ACCESSIBILITY_API_BASE_URL = values.Value(
|
||||
None,
|
||||
environ_name="ACCESSIBILITY_API_BASE_URL",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
AI_DOCUMENT_RATE_THROTTLE_RATES = {
|
||||
"minute": 5,
|
||||
"hour": 100,
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-02-06 15:30+0000\n"
|
||||
"PO-Revision-Date: 2025-02-06 15:59\n"
|
||||
"PO-Revision-Date: 2025-02-10 14:14\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Language: de_DE\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-02-06 15:30+0000\n"
|
||||
"PO-Revision-Date: 2025-02-06 15:57\n"
|
||||
"PO-Revision-Date: 2025-02-10 14:14\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: English\n"
|
||||
"Language: en_US\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-02-06 15:30+0000\n"
|
||||
"PO-Revision-Date: 2025-02-06 15:59\n"
|
||||
"PO-Revision-Date: 2025-02-10 14:14\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Language: fr_FR\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-02-06 15:30+0000\n"
|
||||
"PO-Revision-Date: 2025-02-06 15:57\n"
|
||||
"PO-Revision-Date: 2025-02-10 14:14\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Dutch\n"
|
||||
"Language: nl_NL\n"
|
||||
|
||||
@@ -41,8 +41,16 @@ test.describe('Doc Export', () => {
|
||||
await expect(page.getByRole('button', { name: 'Download' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('it exports the doc to pdf', async ({ page, browserName }) => {
|
||||
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
|
||||
test('it exports the doc with pdf line break', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
const [randomDoc] = await createDoc(
|
||||
page,
|
||||
'doc-editor-line-break',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
const downloadPromise = page.waitForEvent('download', (download) => {
|
||||
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
|
||||
@@ -50,8 +58,20 @@ test.describe('Doc Export', () => {
|
||||
|
||||
await verifyDocName(page, randomDoc);
|
||||
|
||||
await page.locator('.ProseMirror.bn-editor').click();
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
|
||||
const editor = page.locator('.ProseMirror.bn-editor');
|
||||
|
||||
await editor.click();
|
||||
await editor.locator('.bn-block-outer').last().fill('Hello');
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
await editor.locator('.bn-block-outer').last().fill('/');
|
||||
await page.getByText('Page Break').click();
|
||||
|
||||
await expect(editor.locator('.bn-page-break')).toBeVisible();
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await editor.locator('.bn-block-outer').last().fill('World');
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
@@ -69,9 +89,10 @@ test.describe('Doc Export', () => {
|
||||
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
|
||||
|
||||
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
|
||||
const pdfText = (await pdf(pdfBuffer)).text;
|
||||
const pdfData = await pdf(pdfBuffer);
|
||||
|
||||
expect(pdfText).toContain('Hello World'); // This is the doc text
|
||||
expect(pdfData.numpages).toBe(2);
|
||||
expect(pdfData.text).toContain('\n\nHello\n\nWorld'); // This is the doc text
|
||||
});
|
||||
|
||||
test('it exports the doc to docx', async ({ page, browserName }) => {
|
||||
|
||||
@@ -395,9 +395,7 @@ test.describe('Doc Header', () => {
|
||||
navigator.clipboard.readText(),
|
||||
);
|
||||
const clipboardContent = await handle.jsonValue();
|
||||
expect(clipboardContent.trim()).toBe(
|
||||
`<h1 data-level=\"1\">Hello World</h1><p></p>`,
|
||||
);
|
||||
expect(clipboardContent.trim()).toBe(`<h1>Hello World</h1><p></p>`);
|
||||
});
|
||||
|
||||
test('it checks the copy link button', async ({ page }) => {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"test:ui::chromium": "yarn test:ui --project=chromium"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.50.0",
|
||||
"@playwright/test": "1.50.1",
|
||||
"@types/node": "*",
|
||||
"@types/pdf-parse": "1.1.4",
|
||||
"eslint-config-impress": "*",
|
||||
|
||||
@@ -15,34 +15,35 @@
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blocknote/core": "0.21.0",
|
||||
"@blocknote/mantine": "0.21.0",
|
||||
"@blocknote/react": "0.21.0",
|
||||
"@blocknote/xl-docx-exporter": "0.21.0",
|
||||
"@blocknote/xl-pdf-exporter": "0.21.0",
|
||||
"@blocknote/core": "0.23.2",
|
||||
"@blocknote/mantine": "0.23.2",
|
||||
"@blocknote/react": "0.23.2",
|
||||
"@blocknote/xl-docx-exporter": "0.23.2",
|
||||
"@blocknote/xl-pdf-exporter": "0.23.2",
|
||||
"@gouvfr-lasuite/integration": "1.0.2",
|
||||
"@hocuspocus/provider": "2.15.1",
|
||||
"@hocuspocus/provider": "2.15.2",
|
||||
"@openfun/cunningham-react": "2.9.4",
|
||||
"@react-pdf/renderer": "4.1.6",
|
||||
"@sentry/nextjs": "8.52.0",
|
||||
"@tanstack/react-query": "5.65.1",
|
||||
"@sentry/nextjs": "8.54.0",
|
||||
"@tanstack/react-query": "5.66.0",
|
||||
"cmdk": "1.0.4",
|
||||
"crisp-sdk-web": "1.0.25",
|
||||
"docx": "9.1.1",
|
||||
"i18next": "24.2.2",
|
||||
"i18next-browser-languagedetector": "8.0.2",
|
||||
"idb": "8.0.1",
|
||||
"idb": "8.0.2",
|
||||
"lodash": "4.17.21",
|
||||
"luxon": "3.5.0",
|
||||
"marked": "^15.0.7",
|
||||
"next": "15.1.6",
|
||||
"posthog-js": "1.211.3",
|
||||
"posthog-js": "1.215.6",
|
||||
"react": "*",
|
||||
"react-aria-components": "1.6.0",
|
||||
"react-dom": "*",
|
||||
"react-i18next": "15.4.0",
|
||||
"react-intersection-observer": "9.15.1",
|
||||
"react-select": "5.10.0",
|
||||
"styled-components": "6.1.14",
|
||||
"styled-components": "6.1.15",
|
||||
"use-debounce": "10.0.4",
|
||||
"y-protocols": "1.0.6",
|
||||
"yjs": "13.6.23",
|
||||
@@ -50,7 +51,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "8.1.0",
|
||||
"@tanstack/react-query-devtools": "5.65.1",
|
||||
"@tanstack/react-query-devtools": "5.66.0",
|
||||
"@testing-library/dom": "10.4.0",
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@testing-library/react": "16.2.0",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.9 KiB |
@@ -0,0 +1,39 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
|
||||
export type DocAIPdfTranscribe = {
|
||||
docId: string;
|
||||
pdfUrl: string;
|
||||
};
|
||||
|
||||
export type DocAIPdfTranscribeResponse = {
|
||||
document_id: string;
|
||||
};
|
||||
|
||||
export const docAIPdfTranscribe = async ({
|
||||
docId,
|
||||
...params
|
||||
}: DocAIPdfTranscribe): Promise<DocAIPdfTranscribeResponse> => {
|
||||
const response = await fetchAPI(`documents/${docId}/ai-pdf-transcribe/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
...params,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError(
|
||||
'Failed to request pdf transcription',
|
||||
await errorCauses(response),
|
||||
);
|
||||
}
|
||||
|
||||
return response.json() as Promise<DocAIPdfTranscribeResponse>;
|
||||
};
|
||||
|
||||
export function useDocAIPdfTranscribe() {
|
||||
return useMutation<DocAIPdfTranscribeResponse, APIError, DocAIPdfTranscribe>({
|
||||
mutationFn: docAIPdfTranscribe,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
useBlockNoteEditor,
|
||||
useComponentsContext,
|
||||
useSelectedBlocks,
|
||||
} from '@blocknote/react';
|
||||
import {
|
||||
Loader,
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Text } from '@/components';
|
||||
import { useDocStore } from '@/features/docs/doc-management/';
|
||||
|
||||
import { useDocAIPdfTranscribe } from '../api/useDocAIPdfTranscribe';
|
||||
|
||||
export const AIPdfButton = () => {
|
||||
const editor = useBlockNoteEditor();
|
||||
const Components = useComponentsContext();
|
||||
const selectedBlocks = useSelectedBlocks(editor);
|
||||
const { t } = useTranslation();
|
||||
const { currentDoc } = useDocStore();
|
||||
const { toast } = useToastProvider();
|
||||
const router = useRouter();
|
||||
const { mutateAsync: requestAIPdf, isPending } = useDocAIPdfTranscribe();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
if (!Components || !currentDoc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const show = selectedBlocks.length === 1 && selectedBlocks[0].type === 'file';
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handlePdfTranscription = async () => {
|
||||
console.log('selectedBlocks', selectedBlocks);
|
||||
const pdfBlock = selectedBlocks[0];
|
||||
const props = pdfBlock.props as { url?: string };
|
||||
const pdfUrl = props?.url;
|
||||
console.log('pdfUrl', pdfUrl);
|
||||
if (!props || !pdfUrl) {
|
||||
toast(t('No PDF file found'), VariantType.ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await requestAIPdf({
|
||||
docId: currentDoc.id,
|
||||
pdfUrl,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
// router.push causes the following error:
|
||||
// TypeError: Cannot read properties of undefined (reading 'isDestroyed')
|
||||
// void router.push(`/docs/${response.document_id}`);
|
||||
window.location.href = `/docs/${response.document_id}?albert=true`;
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error('error', error);
|
||||
toast(t('Failed to transcribe PDF'), VariantType.ERROR);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Components.FormattingToolbar.Button
|
||||
className="bn-button bn-menu-item"
|
||||
data-test="ai-pdf-transcribe"
|
||||
label="AI"
|
||||
mainTooltip={t('Transcribe PDF')}
|
||||
icon={
|
||||
isLoading ? (
|
||||
<Loader size="small" />
|
||||
) : (
|
||||
<Text $isMaterialIcon $size="l">
|
||||
auto_awesome
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
onClick={() => void handlePdfTranscription()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,303 @@
|
||||
import { Button, Input, Loader } from '@openfun/cunningham-react';
|
||||
import { marked } from 'marked';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { fetchAPI } from '@/api';
|
||||
import { Box, Text } from '@/components';
|
||||
import { Doc } from '@/features/docs';
|
||||
|
||||
import { useEditorStore } from '../../stores/useEditorStore';
|
||||
|
||||
export const AIButtonEl = styled.button`
|
||||
background-image: url('/assets/ia_baguette.png');
|
||||
background-size: cover;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
`;
|
||||
|
||||
export const SuggestionButton = styled.button`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
color: var(--c--theme--colors--greyscale-900);
|
||||
height: 32px;
|
||||
padding: 0 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--c--theme--colors--greyscale-100);
|
||||
}
|
||||
|
||||
span.material-icons {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
span.sub {
|
||||
color: var(--c--theme--colors--greyscale-600);
|
||||
margin-left: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
`;
|
||||
|
||||
export const AiButton = ({ doc }: { doc: Doc }) => {
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(searchParams.get('albert') === 'true');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
$position="absolute"
|
||||
$css={`
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 1rem;
|
||||
margin: 1rem;
|
||||
z-index: 1;
|
||||
|
||||
`}
|
||||
>
|
||||
<AIButtonEl
|
||||
aria-label="Posez une question à Albert à propos de ce document"
|
||||
onClick={() => setIsOpen(true)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<AiChat doc={doc} isOpen={isOpen} onClose={() => setIsOpen(false)} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
type Message = {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
};
|
||||
|
||||
const AiChat = (props: { isOpen: boolean; onClose: () => void; doc: Doc }) => {
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const { editor } = useEditorStore();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
|
||||
if (!props.isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const newPrompt = async (prompt: string) => {
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setMessages([...messages, { role: 'user', content: prompt }]);
|
||||
|
||||
const editorContentFormatted = await editor.blocksToMarkdownLossy();
|
||||
|
||||
const response = await fetchAPI(`documents/${props.doc.id}/ai-proxy/`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
system:
|
||||
'You are a helpful assistant. You are given a text in markdown format and you need to answer the question. Here is the text: ' +
|
||||
editorContentFormatted,
|
||||
text: prompt,
|
||||
}),
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const data = (await response.json()) as string;
|
||||
|
||||
console.log('response', data);
|
||||
setMessages((messages) => [
|
||||
...messages,
|
||||
{ role: 'assistant', content: data },
|
||||
]);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const submit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setPrompt(''); // Clear the prompt after submitting the form
|
||||
await newPrompt(prompt);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
$position="absolute"
|
||||
$css={`
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 1rem;
|
||||
width: 450px;
|
||||
min-height: min(61vh, 365px);
|
||||
box-shadow: rgba(15, 15, 15, 0.04) 0px 0px 0px 1px, rgba(15, 15, 15, 0.03) 0px 3px 6px, rgba(15, 15, 15, 0.06) 0px 9px 24px;
|
||||
max-height: max(-180px + 100vh, 365px);
|
||||
overflow-y: auto;
|
||||
border-radius: 16px;
|
||||
background-color: white;
|
||||
margin: 1rem;
|
||||
z-index: 2;
|
||||
`}
|
||||
$direction="column"
|
||||
>
|
||||
<Box $direction="row" $align="center" $justify="space-between">
|
||||
<Text $theme="greyscale" $variation="1000" $weight="bold" $size="s">
|
||||
{messages.length == 0 ? '' : 'Demander à Albert'}
|
||||
</Text>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={props.onClose}
|
||||
color="tertiary-text"
|
||||
icon={<span className="material-icons">close</span>}
|
||||
/>
|
||||
</Box>
|
||||
{messages.length == 0 && (
|
||||
<Box $gap="1rem" $position="relative" $css="top: -24px;">
|
||||
<Box $gap="0.5rem">
|
||||
<Box
|
||||
$css={`
|
||||
background-image: url('/assets/ia_baguette_question_mark.png');
|
||||
background-size: cover;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
`}
|
||||
></Box>
|
||||
<Text $theme="primary" $variation="800">
|
||||
Bonjour, comment puis-je vous aider ?
|
||||
</Text>
|
||||
</Box>
|
||||
<Box $gap="0.5rem">
|
||||
<Text $theme="greyscale" $variation="1000" $weight="bold" $size="s">
|
||||
Suggestions
|
||||
</Text>
|
||||
<Box>
|
||||
<SuggestionButton
|
||||
onClick={() =>
|
||||
void newPrompt(
|
||||
'Resume ce document sous forme textuelle uniquement',
|
||||
)
|
||||
}
|
||||
>
|
||||
<span className="material-icons">description</span>
|
||||
Résumer <span className="sub">cette page</span>
|
||||
</SuggestionButton>
|
||||
<SuggestionButton
|
||||
onClick={() =>
|
||||
void newPrompt('Quel est le sujet principal de ce document ?')
|
||||
}
|
||||
>
|
||||
<span className="material-icons">help_center</span>
|
||||
Poser des questions <span className="sub">sur cette page</span>
|
||||
</SuggestionButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
$flex={1}
|
||||
$direction="column"
|
||||
$gap="1rem"
|
||||
$css={`
|
||||
overflow-y: auto;
|
||||
font-size: 14px;
|
||||
mask-image: linear-gradient(black calc(100% - 32px), transparent calc(100% - 4px));
|
||||
padding-bottom: 32px;
|
||||
`}
|
||||
aria-live="polite"
|
||||
>
|
||||
{messages.map((message, index) => (
|
||||
<Message key={index} message={message} />
|
||||
))}
|
||||
|
||||
{(isLoading || false) && (
|
||||
<Box $display="flex" $direction="row" $align="center" $gap="0.5rem">
|
||||
<Loader size="small" />
|
||||
Albert réfléchit ...
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
<form onSubmit={(e) => void submit(e)} style={{ width: '100%' }}>
|
||||
<Input
|
||||
type="text"
|
||||
label="Posez votre question"
|
||||
name="prompt"
|
||||
fullWidth={true}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
value={prompt} // Ensure the input value is updated with the state
|
||||
rightIcon={<span className="material-icons">send</span>}
|
||||
/>
|
||||
</form>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const Message = ({ message }: { message: Message }) => {
|
||||
return (
|
||||
<Box>
|
||||
<Box $direction="row" $align="center" $gap="0.5rem">
|
||||
{message.role === 'user' ? (
|
||||
<Box
|
||||
aria-hidden={true}
|
||||
$css={`
|
||||
background-color:#417DC4;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
`}
|
||||
>
|
||||
VD
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
aria-hidden={true}
|
||||
$css={`
|
||||
background-image: url('/assets/ia_baguette.png');
|
||||
background-size: cover;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
`}
|
||||
></Box>
|
||||
)}
|
||||
<Text $weight="bold">
|
||||
{message.role === 'user' ? 'Vous' : 'Albert'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
$css={`
|
||||
font-size: 12px;
|
||||
padding-left: 34px;
|
||||
color: var(--c--theme--colors--greyscale-700);
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
`}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: marked.parse(message.content) as string,
|
||||
}}
|
||||
></Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
+43
-100
@@ -1,4 +1,9 @@
|
||||
import { Dictionary, locales } from '@blocknote/core';
|
||||
import {
|
||||
BlockNoteSchema,
|
||||
Dictionary,
|
||||
locales,
|
||||
withPageBreak,
|
||||
} from '@blocknote/core';
|
||||
import '@blocknote/core/fonts/inter.css';
|
||||
import { BlockNoteView } from '@blocknote/mantine';
|
||||
import '@blocknote/mantine/style.css';
|
||||
@@ -6,7 +11,6 @@ import { useCreateBlockNote } from '@blocknote/react';
|
||||
import { HocuspocusProvider } from '@hocuspocus/provider';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { Box, TextErrors } from '@/components';
|
||||
@@ -17,95 +21,13 @@ import { useUploadFile } from '../hook';
|
||||
import { useHeadings } from '../hook/useHeadings';
|
||||
import useSaveDoc from '../hook/useSaveDoc';
|
||||
import { useEditorStore } from '../stores';
|
||||
import { cssEditor } from '../styles';
|
||||
import { randomColor } from '../utils';
|
||||
|
||||
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
|
||||
import { BlockNoteToolbar } from './BlockNoteToolbar';
|
||||
|
||||
const cssEditor = (readonly: boolean) => css`
|
||||
&,
|
||||
& > .bn-container,
|
||||
& .ProseMirror {
|
||||
height: 100%;
|
||||
|
||||
.bn-side-menu[data-block-type='heading'][data-level='1'] {
|
||||
height: 50px;
|
||||
}
|
||||
.bn-side-menu[data-block-type='heading'][data-level='2'] {
|
||||
height: 43px;
|
||||
}
|
||||
.bn-side-menu[data-block-type='heading'][data-level='3'] {
|
||||
height: 35px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.875rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
a {
|
||||
color: var(--c--theme--colors--greyscale-500);
|
||||
cursor: pointer;
|
||||
}
|
||||
.bn-block-group
|
||||
.bn-block-group
|
||||
.bn-block-outer:not([data-prev-depth-changed]):before {
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bn-editor {
|
||||
color: var(--c--theme--colors--greyscale-700);
|
||||
}
|
||||
|
||||
.bn-block-outer:not(:first-child) {
|
||||
&:has(h1) {
|
||||
padding-top: 32px;
|
||||
}
|
||||
&:has(h2) {
|
||||
padding-top: 24px;
|
||||
}
|
||||
&:has(h3) {
|
||||
padding-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
& .bn-inline-content code {
|
||||
background-color: gainsboro;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@media screen and (width <= 560px) {
|
||||
& .bn-editor {
|
||||
${readonly && `padding-left: 10px;`}
|
||||
}
|
||||
.bn-side-menu[data-block-type='heading'][data-level='1'] {
|
||||
height: 46px;
|
||||
}
|
||||
.bn-side-menu[data-block-type='heading'][data-level='2'] {
|
||||
height: 40px;
|
||||
}
|
||||
.bn-side-menu[data-block-type='heading'][data-level='3'] {
|
||||
height: 40px;
|
||||
}
|
||||
& .bn-editor h1 {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
& .bn-editor h2 {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
& .bn-editor h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
|
||||
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const blockNoteSchema = withPageBreak(BlockNoteSchema.create());
|
||||
|
||||
interface BlockNoteEditorProps {
|
||||
doc: Doc;
|
||||
@@ -127,6 +49,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
const collabName = readOnly
|
||||
? 'Reader'
|
||||
: user?.full_name || user?.email || t('Anonymous');
|
||||
const showCursorLabels: 'always' | 'activity' | (string & {}) = 'activity';
|
||||
|
||||
const editor = useCreateBlockNote(
|
||||
{
|
||||
@@ -138,33 +61,50 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
color: randomColor(),
|
||||
},
|
||||
/**
|
||||
* We re-use the blocknote code to render the cursor but we:
|
||||
* - fix rendering issue with Firefox
|
||||
* - We don't want to show the cursor when anonymous users
|
||||
* We render the cursor with a custom element to:
|
||||
* - fix rendering issue with the default cursor
|
||||
* - hide the cursor when anonymous users
|
||||
*/
|
||||
renderCursor: (user: { color: string; name: string }) => {
|
||||
const cursor = document.createElement('span');
|
||||
const cursorElement = document.createElement('span');
|
||||
|
||||
if (user.name === 'Reader') {
|
||||
return cursor;
|
||||
return cursorElement;
|
||||
}
|
||||
|
||||
cursor.classList.add('collaboration-cursor__caret');
|
||||
cursor.setAttribute('style', `border-color: ${user.color}`);
|
||||
cursorElement.classList.add('collaboration-cursor-custom__base');
|
||||
const caretElement = document.createElement('span');
|
||||
caretElement.classList.add('collaboration-cursor-custom__caret');
|
||||
caretElement.setAttribute('spellcheck', `false`);
|
||||
caretElement.setAttribute('style', `background-color: ${user.color}`);
|
||||
|
||||
const label = document.createElement('span');
|
||||
if (showCursorLabels === 'always') {
|
||||
cursorElement.setAttribute('data-active', '');
|
||||
}
|
||||
|
||||
label.classList.add('collaboration-cursor__label');
|
||||
label.setAttribute('style', `background-color: ${user.color}`);
|
||||
label.insertBefore(document.createTextNode(user.name), null);
|
||||
const labelElement = document.createElement('span');
|
||||
|
||||
cursor.insertBefore(label, null);
|
||||
labelElement.classList.add('collaboration-cursor-custom__label');
|
||||
labelElement.setAttribute('spellcheck', `false`);
|
||||
labelElement.setAttribute(
|
||||
'style',
|
||||
`background-color: ${user.color};border: 1px solid ${user.color};`,
|
||||
);
|
||||
labelElement.insertBefore(document.createTextNode(user.name), null);
|
||||
|
||||
return cursor;
|
||||
caretElement.insertBefore(labelElement, null);
|
||||
|
||||
cursorElement.insertBefore(document.createTextNode('\u2060'), null); // Non-breaking space
|
||||
cursorElement.insertBefore(caretElement, null);
|
||||
cursorElement.insertBefore(document.createTextNode('\u2060'), null); // Non-breaking space
|
||||
|
||||
return cursorElement;
|
||||
},
|
||||
showCursorLabels: showCursorLabels as 'always' | 'activity',
|
||||
},
|
||||
dictionary: locales[lang as keyof typeof locales] as Dictionary,
|
||||
uploadFile,
|
||||
schema: blockNoteSchema,
|
||||
},
|
||||
[collabName, lang, provider, uploadFile],
|
||||
);
|
||||
@@ -197,10 +137,12 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
<BlockNoteView
|
||||
editor={editor}
|
||||
formattingToolbar={false}
|
||||
slashMenu={false}
|
||||
editable={!readOnly}
|
||||
theme="light"
|
||||
>
|
||||
<BlockNoteToolbar />
|
||||
<BlockNoteSuggestionMenu />
|
||||
</BlockNoteView>
|
||||
</Box>
|
||||
);
|
||||
@@ -225,6 +167,7 @@ export const BlockNoteEditorVersion = ({
|
||||
},
|
||||
provider: undefined,
|
||||
},
|
||||
schema: blockNoteSchema,
|
||||
},
|
||||
[initialContent],
|
||||
);
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { combineByGroup, filterSuggestionItems } from '@blocknote/core';
|
||||
import '@blocknote/mantine/style.css';
|
||||
import {
|
||||
SuggestionMenuController,
|
||||
getDefaultReactSlashMenuItems,
|
||||
getPageBreakReactSlashMenuItems,
|
||||
useBlockNoteEditor,
|
||||
} from '@blocknote/react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { DocsBlockNoteEditor } from '../types';
|
||||
|
||||
export const BlockNoteSuggestionMenu = () => {
|
||||
const editor = useBlockNoteEditor() as DocsBlockNoteEditor;
|
||||
|
||||
const getSlashMenuItems = useMemo(() => {
|
||||
return async (query: string) =>
|
||||
Promise.resolve(
|
||||
filterSuggestionItems(
|
||||
combineByGroup(
|
||||
getDefaultReactSlashMenuItems(editor),
|
||||
getPageBreakReactSlashMenuItems(editor),
|
||||
),
|
||||
query,
|
||||
),
|
||||
);
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<SuggestionMenuController
|
||||
triggerCharacter="/"
|
||||
getItems={getSlashMenuItems}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+15
-9
@@ -8,21 +8,27 @@ import {
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { AIGroupButton } from './AIButton';
|
||||
import { AIPdfButton } from './AIPdfButton';
|
||||
import { MarkdownButton } from './MarkdownButton';
|
||||
|
||||
export const BlockNoteToolbar = () => {
|
||||
const formattingToolbar = useCallback(
|
||||
({ blockTypeSelectItems }: FormattingToolbarProps) => (
|
||||
<FormattingToolbar>
|
||||
{getFormattingToolbarItems(blockTypeSelectItems)}
|
||||
({ blockTypeSelectItems }: FormattingToolbarProps) => {
|
||||
console.log('formattingToolbar', blockTypeSelectItems);
|
||||
return (
|
||||
<FormattingToolbar>
|
||||
{getFormattingToolbarItems(blockTypeSelectItems)}
|
||||
|
||||
{/* Extra button to do some AI powered actions */}
|
||||
<AIGroupButton key="AIButton" />
|
||||
{/* Extra button to do some AI powered actions */}
|
||||
<AIGroupButton key="AIButton" />
|
||||
|
||||
{/* Extra button to convert from markdown to json */}
|
||||
<MarkdownButton key="customButton" />
|
||||
</FormattingToolbar>
|
||||
),
|
||||
{/* Extra button to convert from markdown to json */}
|
||||
<MarkdownButton key="customButton" />
|
||||
|
||||
<AIPdfButton />
|
||||
</FormattingToolbar>
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { TableContent } from '@/features/docs/doc-table-content/';
|
||||
import { Versions, useDocVersion } from '@/features/docs/doc-versioning/';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { AiButton } from './Ai/AiButton';
|
||||
import { BlockNoteEditor, BlockNoteEditorVersion } from './BlockNoteEditor';
|
||||
|
||||
interface DocEditorProps {
|
||||
@@ -38,6 +39,7 @@ export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<AiButton doc={doc} />
|
||||
{isDesktop && !isVersion && (
|
||||
<Box
|
||||
$position="absolute"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { BlockNoteEditor } from '@blocknote/core';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useHeadingStore } from '../stores';
|
||||
import { DocsBlockNoteEditor } from '../types';
|
||||
|
||||
export const useHeadings = (editor: BlockNoteEditor) => {
|
||||
export const useHeadings = (editor: DocsBlockNoteEditor) => {
|
||||
const { setHeadings, resetHeadings } = useHeadingStore();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { BlockNoteEditor } from '@blocknote/core';
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { DocsBlockNoteEditor } from '../types';
|
||||
|
||||
export interface UseEditorstore {
|
||||
editor?: BlockNoteEditor;
|
||||
setEditor: (editor: BlockNoteEditor | undefined) => void;
|
||||
editor?: DocsBlockNoteEditor;
|
||||
setEditor: (editor: DocsBlockNoteEditor | undefined) => void;
|
||||
}
|
||||
|
||||
export const useEditorStore = create<UseEditorstore>((set) => ({
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { BlockNoteEditor } from '@blocknote/core';
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { HeadingBlock } from '../types';
|
||||
import { DocsBlockNoteEditor, HeadingBlock } from '../types';
|
||||
|
||||
const recursiveTextContent = (content: HeadingBlock['content']): string => {
|
||||
if (!content) {
|
||||
@@ -21,7 +20,7 @@ const recursiveTextContent = (content: HeadingBlock['content']): string => {
|
||||
|
||||
export interface UseHeadingStore {
|
||||
headings: HeadingBlock[];
|
||||
setHeadings: (editor: BlockNoteEditor) => void;
|
||||
setHeadings: (editor: DocsBlockNoteEditor) => void;
|
||||
resetHeadings: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { css } from 'styled-components';
|
||||
|
||||
export const cssEditor = (readonly: boolean) => css`
|
||||
&,
|
||||
& > .bn-container,
|
||||
& .ProseMirror {
|
||||
height: 100%;
|
||||
|
||||
.collaboration-cursor-custom__base {
|
||||
position: relative;
|
||||
}
|
||||
.collaboration-cursor-custom__caret {
|
||||
position: absolute;
|
||||
height: 85%;
|
||||
width: 2px;
|
||||
bottom: 4%;
|
||||
left: -1px;
|
||||
}
|
||||
.collaboration-cursor-custom__label {
|
||||
color: #0d0d0d;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
top: -17px;
|
||||
padding: 0px 6px;
|
||||
border-radius: 0px;
|
||||
white-space: nowrap;
|
||||
transition: clip-path 0.3s ease-in-out;
|
||||
border-radius: 4px 4px 4px 0;
|
||||
box-shadow: inset -2px 2px 6px #ffffff00;
|
||||
clip-path: polygon(0 85%, 4% 85%, 4% 100%, 0% 100%);
|
||||
}
|
||||
.collaboration-cursor-custom__base[data-active]
|
||||
.collaboration-cursor-custom__label {
|
||||
pointer-events: none;
|
||||
box-shadow: inset -2px 2px 6px #ffffff88;
|
||||
clip-path: polygon(0 0, 100% 0%, 100% 100%, 0% 100%);
|
||||
}
|
||||
|
||||
.bn-side-menu[data-block-type='heading'][data-level='1'] {
|
||||
height: 50px;
|
||||
}
|
||||
.bn-side-menu[data-block-type='heading'][data-level='2'] {
|
||||
height: 43px;
|
||||
}
|
||||
.bn-side-menu[data-block-type='heading'][data-level='3'] {
|
||||
height: 35px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.875rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
a {
|
||||
color: var(--c--theme--colors--greyscale-500);
|
||||
cursor: pointer;
|
||||
}
|
||||
.bn-block-group
|
||||
.bn-block-group
|
||||
.bn-block-outer:not([data-prev-depth-changed]):before {
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bn-editor {
|
||||
color: var(--c--theme--colors--greyscale-700);
|
||||
}
|
||||
|
||||
.bn-block-outer:not(:first-child) {
|
||||
&:has(h1) {
|
||||
padding-top: 32px;
|
||||
}
|
||||
&:has(h2) {
|
||||
padding-top: 24px;
|
||||
}
|
||||
&:has(h3) {
|
||||
padding-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
& .bn-inline-content code {
|
||||
background-color: gainsboro;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@media screen and (width <= 560px) {
|
||||
& .bn-editor {
|
||||
${readonly && `padding-left: 10px;`}
|
||||
}
|
||||
.bn-side-menu[data-block-type='heading'][data-level='1'] {
|
||||
height: 46px;
|
||||
}
|
||||
.bn-side-menu[data-block-type='heading'][data-level='2'] {
|
||||
height: 40px;
|
||||
}
|
||||
.bn-side-menu[data-block-type='heading'][data-level='3'] {
|
||||
height: 40px;
|
||||
}
|
||||
& .bn-editor h1 {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
& .bn-editor h2 {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
& .bn-editor h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
|
||||
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,3 +1,7 @@
|
||||
import { BlockNoteEditor } from '@blocknote/core';
|
||||
|
||||
import { blockNoteSchema } from './components/BlockNoteEditor';
|
||||
|
||||
export interface DocAttachment {
|
||||
file: string;
|
||||
}
|
||||
@@ -12,3 +16,9 @@ export type HeadingBlock = {
|
||||
level: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type DocsBlockNoteEditor = BlockNoteEditor<
|
||||
typeof blockNoteSchema.blockSchema,
|
||||
typeof blockNoteSchema.inlineContentSchema,
|
||||
typeof blockNoteSchema.styleSchema
|
||||
>;
|
||||
|
||||
@@ -98,7 +98,61 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
|
||||
const exporter = new PDFExporter(
|
||||
editor.schema,
|
||||
pdfDefaultSchemaMappings,
|
||||
{
|
||||
...pdfDefaultSchemaMappings,
|
||||
blockMapping: {
|
||||
...pdfDefaultSchemaMappings.blockMapping,
|
||||
heading: (block, exporter) => {
|
||||
const PIXELS_PER_POINT = 0.75;
|
||||
const MERGE_RATIO = 7.5;
|
||||
const FONT_SIZE = 16;
|
||||
const fontSizeEM =
|
||||
block.props.level === 1
|
||||
? 2
|
||||
: block.props.level === 2
|
||||
? 1.5
|
||||
: 1.17;
|
||||
return (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: fontSizeEM * FONT_SIZE * PIXELS_PER_POINT,
|
||||
fontWeight: 700,
|
||||
marginTop: `${fontSizeEM * MERGE_RATIO}px`,
|
||||
marginBottom: `${fontSizeEM * MERGE_RATIO}px`,
|
||||
}}
|
||||
>
|
||||
{exporter.transformInlineContent(block.content)}
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
paragraph: (block, exporter) => {
|
||||
/**
|
||||
* Breakline in the editor are not rendered in the PDF
|
||||
* By adding a space if the block is empty we ensure that the block is rendered
|
||||
*/
|
||||
if (Array.isArray(block.content)) {
|
||||
block.content.forEach((content) => {
|
||||
if (content.type === 'text' && !content.text) {
|
||||
content.text = ' ';
|
||||
}
|
||||
});
|
||||
|
||||
if (!block.content.length) {
|
||||
block.content.push({
|
||||
styles: {},
|
||||
text: ' ',
|
||||
type: 'text',
|
||||
});
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Text key={block.id}>
|
||||
{exporter.transformInlineContent(block.content)}
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
resolveFileUrl: async (url) =>
|
||||
exportResolveFileUrl(url, defaultExporter.options.resolveFileUrl),
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { BlockNoteEditor } from '@blocknote/core';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { BoxButton, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { DocsBlockNoteEditor } from '@/features/docs/doc-editor';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
const leftPaddingMap: { [key: number]: string } = {
|
||||
@@ -17,7 +17,7 @@ export type HeadingsHighlight = {
|
||||
}[];
|
||||
|
||||
interface HeadingProps {
|
||||
editor: BlockNoteEditor;
|
||||
editor: DocsBlockNoteEditor;
|
||||
level: number;
|
||||
text: string;
|
||||
headingId: string;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<svg
|
||||
width="24"
|
||||
height="25"
|
||||
viewBox="0 0 24 25"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_7060_4428)">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12 0.0153809C5.37 0.0153809 0 5.38538 0 12.0154C0 17.3254 3.435 21.8104 8.205 23.4004C8.805 23.5054 9.03 23.1454 9.03 22.8304C9.03 22.5454 9.015 21.6004 9.015 20.5954C6 21.1504 5.22 19.8604 4.98 19.1854C4.845 18.8404 4.26 17.7754 3.75 17.4904C3.33 17.2654 2.73 16.7104 3.735 16.6954C4.68 16.6804 5.355 17.5654 5.58 17.9254C6.66 19.7404 8.385 19.2304 9.075 18.9154C9.18 18.1354 9.495 17.6104 9.84 17.3104C7.17 17.0104 4.38 15.9754 4.38 11.3854C4.38 10.0804 4.845 9.00038 5.61 8.16038C5.49 7.86038 5.07 6.63038 5.73 4.98038C5.73 4.98038 6.735 4.66538 9.03 6.21038C9.99 5.94038 11.01 5.80538 12.03 5.80538C13.05 5.80538 14.07 5.94038 15.03 6.21038C17.325 4.65038 18.33 4.98038 18.33 4.98038C18.99 6.63038 18.57 7.86038 18.45 8.16038C19.215 9.00038 19.68 10.0654 19.68 11.3854C19.68 15.9904 16.875 17.0104 14.205 17.3104C14.64 17.6854 15.015 18.4054 15.015 19.5304C15.015 21.1354 15 22.4254 15 22.8304C15 23.1454 15.225 23.5204 15.825 23.4004C20.565 21.8104 24 17.3104 24 12.0154C24 5.38538 18.63 0.0153809 12 0.0153809Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7060_4428">
|
||||
<rect
|
||||
width="24"
|
||||
height="24"
|
||||
fill="white"
|
||||
transform="translate(0 0.0153809)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -1,3 +1,4 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import Image from 'next/image';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
@@ -10,6 +11,7 @@ import { Title } from '@/features/header';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import SC5 from '../assets/SC5.png';
|
||||
import GithubIcon from '../assets/github.svg';
|
||||
|
||||
import { HomeSection } from './HomeSection';
|
||||
|
||||
@@ -72,7 +74,7 @@ function HomeOpenSource() {
|
||||
</Text>
|
||||
<Text as="p" $display="inline">
|
||||
<Trans t={t} i18nKey="home-content-open-source-part2">
|
||||
You can easily self-host Docs (check our installation{' '}
|
||||
You can easily self-hosted Docs (check our installation{' '}
|
||||
<a
|
||||
href="https://github.com/suitenumerique/docs/tree/main/docs"
|
||||
target="_blank"
|
||||
@@ -114,6 +116,27 @@ function HomeOpenSource() {
|
||||
are interested in using or contributing to docs.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Box $direction="row" $gap="1rem" $margin={{ top: 'small' }}>
|
||||
<Button
|
||||
icon={
|
||||
<Text $isMaterialIcon $color="white">
|
||||
chat
|
||||
</Text>
|
||||
}
|
||||
href="https://matrix.to/#/#docs-official:matrix.org"
|
||||
target="_blank"
|
||||
>
|
||||
<Text $color="white">Matrix</Text>
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
icon={<GithubIcon />}
|
||||
href="https://github.com/suitenumerique/docs"
|
||||
target="_blank"
|
||||
>
|
||||
Github
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -167,6 +167,7 @@
|
||||
"translation": {
|
||||
"\"{{email}}\" is already invited to the document.": "\"{{email}}\" est déjà invité à accéder au document.",
|
||||
"\"{{email}}\" is already member of the document.": "\"{{email}}\" est déjà membre du document.",
|
||||
"A new way to organize knowledge.": "Une nouvelle façon d’organiser les connaissances.",
|
||||
"AI Actions": "Actions IA",
|
||||
"AI seems busy! Please try again.": "L'IA semble occupée ! Veuillez réessayer.",
|
||||
"Accessibility": "Accessibilité",
|
||||
@@ -177,16 +178,22 @@
|
||||
"Address:": "Adresse :",
|
||||
"Administrator": "Administrateur",
|
||||
"All docs": "Tous les documents",
|
||||
"An uncompromising writing experience.": "Une expérience d'écriture sans compromis.",
|
||||
"Anonymous": "Anonyme",
|
||||
"Anyone with the link can edit the document": "N'importe qui avec le lien peut éditer le document",
|
||||
"Anyone with the link can edit the document if they are logged in": "N'importe qui avec le lien peut éditer le document à condition qu'il soit connecté",
|
||||
"Anyone with the link can see the document": "N'importe qui avec le lien peut voir le document",
|
||||
"Anyone with the link can view the document if they are logged in": "N'importe qui avec le lien peut voir le document à condition qu'il soit connecté",
|
||||
"Are you sure you want to delete the document \"{{title}}\"?": "Êtes-vous sûr de vouloir supprimer le document \"{{title}}\" ?",
|
||||
"Available soon": "Disponible prochainement",
|
||||
"Back to home page": "Retour à l'accueil",
|
||||
"Banner image": "Image de la bannière",
|
||||
"Can't load this page, please check your internet connection.": "Impossible de charger cette page, veuillez vérifier votre connexion Internet.",
|
||||
"Cancel": "Annuler",
|
||||
"Close the modal": "Fermer la modale",
|
||||
"Collaborate": "Collaborer",
|
||||
"Collaborate and write in real time, without layout constraints.": "Collaborez et rédigez en temps réel, sans contrainte de mise en page.",
|
||||
"Collaborative writing, Simplified.": "L'écriture collaborative simplifiée.",
|
||||
"Compliance status": "État de conformité",
|
||||
"Confirm deletion": "Confirmer la suppression",
|
||||
"Connected": "Connecté",
|
||||
@@ -206,6 +213,10 @@
|
||||
"Doc visibility card": "Carte de visibilité du doc",
|
||||
"Docs": "Docs",
|
||||
"Docs Logo": "Logo Docs",
|
||||
"Docs is already available, log in to use it now.": "Docs est déjà disponible, connectez-vous pour l’utiliser dès maintenant.",
|
||||
"Docs makes real-time collaboration simple. Invite collaborators - public officials or external partners - with one click to see their changes live, while maintaining precise access control for data security.": "Docs simplifie la collaboration en temps réel. Invitez des collaborateurs - agents publics ou partenaires externes - d'un clic pour voir leurs modifications en direct, tout en gardant un contrôle précis des accès pour la sécurité des données.",
|
||||
"Docs offers an intuitive writing experience. Its minimalist interface favors content over layout, while offering the essentials: media import, offline mode and keyboard shortcuts for greater efficiency.": "Docs propose une expérience d'écriture intuitive. Son interface minimaliste privilégie le contenu sur la mise en page, tout en offrant l'essentiel : import de médias, mode hors-ligne et raccourcis clavier pour plus d'efficacité.",
|
||||
"Docs transforms your documents into knowledge bases thanks to subpages, powerful search and the ability to pin your important documents.": "Docs transforme vos documents en bases de connaissances grâce aux sous-pages, une recherche performante et la possibilité d'épingler vos documents importants.",
|
||||
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs : Votre nouveau compagnon pour collaborer sur des documents efficacement, intuitivement et en toute sécurité.",
|
||||
"Document owner": "Propriétaire du document",
|
||||
"Document title updated successfully": "Titre du document mis à jour avec succès",
|
||||
@@ -227,11 +238,14 @@
|
||||
"Failed to copy link": "Échec de la copie du lien",
|
||||
"Failed to copy to clipboard": "Échec de la copie dans le presse-papier",
|
||||
"Failed to create the invitation for {{email}}.": "Impossible de créer l'invitation pour {{email}}.",
|
||||
"Flexible export.": "Un export flexible.",
|
||||
"Format": "Format",
|
||||
"French Interministerial Directorate for Digital Affairs (DINUM), 20 avenue de Ségur 75007 Paris.": "Direction interministérielle des affaires numériques (DINUM), 20 avenue de Segur 75007 Paris.",
|
||||
"Govs ❤️ Open Source.": "Gouvs ❤️ Open Source.",
|
||||
"History": "Historique",
|
||||
"If a member is editing, his works can be lost.": "Si un membre est en train d'éditer, ses travaux peuvent être perdus.",
|
||||
"If you are unable to access a content or a service, you can contact the person responsible for https://lasuite.numerique.gouv.fr to be directed to an accessible alternative or to obtain the content in another form.": "Si vous ne pouvez pas accéder à un contenu ou à un service, vous pouvez contacter la personne responsable de https://lasuite. umerique.gouv.fr pour être dirigé vers une alternative accessible ou pour obtenir le contenu sous une autre forme.",
|
||||
"Illustration": "Image",
|
||||
"Illustration:": "Illustration :",
|
||||
"Improvement and contact": "Amélioration et contact",
|
||||
"Invite": "Inviter",
|
||||
@@ -264,8 +278,10 @@
|
||||
"OK": "OK",
|
||||
"Offline ?!": "Hors-ligne ?!",
|
||||
"Only invited people can access": "Seules les personnes invitées peuvent accéder",
|
||||
"Open Source": "Open Source",
|
||||
"Open the document options": "Ouvrir les options du document",
|
||||
"Open the header menu": "Ouvrir le menu d'en-tête",
|
||||
"Organize": "Organiser",
|
||||
"Ouch !": "Aïe !",
|
||||
"Owner": "Propriétaire",
|
||||
"PDF": "PDF",
|
||||
@@ -275,13 +291,15 @@
|
||||
"Pin document icon": "Icône épingler un document",
|
||||
"Pinned documents": "Documents épinglés",
|
||||
"Private": "Privé",
|
||||
"ProConnect Image": "Image ProConnect",
|
||||
"Proconnect Login": "Login Proconnect",
|
||||
"Public": "Public",
|
||||
"Public document": "Document public",
|
||||
"Publication Director": "Directeur de la publication",
|
||||
"Publisher": "Éditeur",
|
||||
"Quick search input": "Saisie de recherche rapide",
|
||||
"Reader": "Lecteur",
|
||||
"Reading": "Lecture seul",
|
||||
"Reading": "Lecture seule",
|
||||
"Remedies": "Voie de recours",
|
||||
"Remove": "Supprimer",
|
||||
"Rename": "Renommer",
|
||||
@@ -303,8 +321,11 @@
|
||||
"Shared with {{count}} users_many": "Partager avec {{count}} utilisateurs",
|
||||
"Shared with {{count}} users_one": "Partager avec {{count}} utilisateur",
|
||||
"Shared with {{count}} users_other": "Partager avec {{count}} utilisateurs",
|
||||
"Show more": "Voir plus",
|
||||
"Simple and secure collaboration.": "Une collaboration simple et sécurisée.",
|
||||
"Simple document icon": "Icône simple du document",
|
||||
"Something bad happens, please retry.": "Une erreur inattendue s'est produite, veuillez réessayer.",
|
||||
"Start Writing": "Commencer à écrire",
|
||||
"Stéphanie Schaer: Interministerial Digital Director (DINUM).": "Stéphanie Schaer: Directrice numérique interministériel (DINUM).",
|
||||
"Summarize": "Résumer",
|
||||
"Summary": "Sommaire",
|
||||
@@ -319,6 +340,7 @@
|
||||
"This site does not display a cookie consent banner, why?": "Ce site n'affiche pas de bannière de consentement des cookies, pourquoi?",
|
||||
"This site places a small text file (a \"cookie\") on your computer when you visit it.": "Ce site place un petit fichier texte (un « cookie ») sur votre ordinateur lorsque vous le visitez.",
|
||||
"This will protect your privacy, but will also prevent the owner from learning from your actions and creating a better experience for you and other users.": "Cela protégera votre vie privée, mais empêchera également le propriétaire d'apprendre de vos actions et de créer une meilleure expérience pour vous et les autres utilisateurs.",
|
||||
"To facilitate the circulation of documents, Docs allows you to export your content to the most common formats: PDF, Word or OpenDocument.": "Pour faciliter la circulation des documents, Docs permet d'exporter vos contenus vers les formats les plus courants : PDF, Word ou OpenDocument.",
|
||||
"Too many requests. Please wait 60 seconds.": "Trop de demandes. Veuillez patienter 60 secondes.",
|
||||
"Type a name or email": "Tapez un nom ou un email",
|
||||
"Type the name of a document": "Tapez le nom d'un document",
|
||||
@@ -334,6 +356,7 @@
|
||||
"Warning": "Attention",
|
||||
"We simply comply with the law, which states that certain audience measurement tools, properly configured to respect privacy, are exempt from prior authorization.": "Nous nous conformons simplement à la loi, qui stipule que certains outils de mesure d’audience, correctement configurés pour respecter la vie privée, sont exemptés de toute autorisation préalable.",
|
||||
"We try to respond within 2 working days.": "Nous essayons de répondre dans les 2 jours ouvrables.",
|
||||
"Write": "Écrire",
|
||||
"You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.": "Vous êtes le seul propriétaire de ce groupe, faites d'un autre membre le propriétaire du groupe, avant de pouvoir modifier votre propre rôle ou vous supprimer du document.",
|
||||
"You can oppose the tracking of your browsing on this website.": "Vous pouvez vous opposer au suivi de votre navigation sur ce site.",
|
||||
"You can:": "Vous pouvez:",
|
||||
@@ -345,6 +368,9 @@
|
||||
"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>",
|
||||
"home-content-open-source-part1": "Docs est construit sur <2>Django Rest Framework</2>, <5>Next.js</5>, et <8>MinIO</8>. Nous utilisons également <11>Yjs</11> et <15>BlockNote.js</15> dont nous sommes fiers sponsors.",
|
||||
"home-content-open-source-part2": "Vous pouvez facilement auto-héberger Docs (consultez notre <2>documentation</2> d'installation avec des exemples prêts pour la production).<br/>Docs utilise une <8>licence</8> adaptée à l'innovation et aux entreprises.<br/>Les contributions sont les bienvenues (consultez notre feuille de route <13>ici</13>).",
|
||||
"home-content-open-source-part3": "Docs est le résultat d'un effort conjoint mené par les gouvernements français 🇫🇷🥖 <1>(DINUM)</1> et allemand 🇩🇪🥨 <5>(ZenDiS)</5>. Nous sommes toujours à la recherche de nouveaux partenaires publics (nous embarquons actuellement les Pays-Bas 🇳🇱🧀). N'hésitez pas à nous contacter si vous souhaitez utiliser ou contribuer à Docs.",
|
||||
"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."
|
||||
}
|
||||
},
|
||||
|
||||
Generated
+25849
File diff suppressed because it is too large
Load Diff
@@ -28,10 +28,10 @@
|
||||
"server:test": "yarn COLLABORATION_SERVER run test"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/node": "22.12.0",
|
||||
"@types/node": "22.13.1",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.22.0",
|
||||
"@typescript-eslint/parser": "8.22.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.23.0",
|
||||
"@typescript-eslint/parser": "8.23.0",
|
||||
"eslint": "8.57.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/eslint-plugin-next": "15.1.6",
|
||||
"@tanstack/eslint-plugin-query": "5.65.0",
|
||||
"@tanstack/eslint-plugin-query": "5.66.0",
|
||||
"@typescript-eslint/eslint-plugin": "*",
|
||||
"@typescript-eslint/parser": "*",
|
||||
"eslint": "*",
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blocknote/server-util": "0.21.0",
|
||||
"@hocuspocus/server": "2.15.1",
|
||||
"@sentry/node": "8.52.0",
|
||||
"@sentry/profiling-node": "8.52.0",
|
||||
"@blocknote/server-util": "0.23.2",
|
||||
"@hocuspocus/server": "2.15.2",
|
||||
"@sentry/node": "8.54.0",
|
||||
"@sentry/profiling-node": "8.54.0",
|
||||
"cors": "2.8.5",
|
||||
"express": "4.21.2",
|
||||
"express-ws": "5.0.2",
|
||||
@@ -27,7 +27,7 @@
|
||||
"yjs": "13.6.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hocuspocus/provider": "2.15.1",
|
||||
"@hocuspocus/provider": "2.15.2",
|
||||
"@types/cors": "2.8.17",
|
||||
"@types/express": "5.0.0",
|
||||
"@types/express-ws": "3.0.5",
|
||||
|
||||
+14650
-14611
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
type: application
|
||||
name: docs
|
||||
version: 2.1.0
|
||||
version: 2.1.0-beta-1
|
||||
appVersion: latest
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
| `backend.persistence.volume-name.mountPath` | Path where the volume should be mounted to | |
|
||||
| `backend.extraVolumeMounts` | Additional volumes to mount on the backend. | `[]` |
|
||||
| `backend.extraVolumes` | Additional volumes to mount on the backend. | `[]` |
|
||||
| `backend.pdb.enabled` | Enable pdb on backend | `true` |
|
||||
|
||||
### frontend
|
||||
|
||||
@@ -180,6 +181,7 @@
|
||||
| `frontend.persistence.volume-name.mountPath` | Path where the volume should be mounted to | |
|
||||
| `frontend.extraVolumeMounts` | Additional volumes to mount on the frontend. | `[]` |
|
||||
| `frontend.extraVolumes` | Additional volumes to mount on the frontend. | `[]` |
|
||||
| `frontend.pdb.enabled` | Enable pdb on frontend | `true` |
|
||||
|
||||
### posthog
|
||||
|
||||
@@ -261,3 +263,4 @@
|
||||
| `yProvider.persistence.volume-name.mountPath` | Path where the volume should be mounted to | |
|
||||
| `yProvider.extraVolumeMounts` | Additional volumes to mount on the yProvider. | `[]` |
|
||||
| `yProvider.extraVolumes` | Additional volumes to mount on the yProvider. | `[]` |
|
||||
| `yProvider.pdb.enabled` | Enable pdb on yProvider | `true` |
|
||||
|
||||
Regular → Executable
+1
-1
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
docker image ls | grep readme-generator-for-helm
|
||||
if [ "$?" -ne "0" ]; then
|
||||
|
||||
@@ -138,3 +138,16 @@ spec:
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
---
|
||||
{{ if .Values.backend.pdb.enabled }}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
spec:
|
||||
maxUnavailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "impress.common.selectorLabels" (list . $component) | nindent 6 }}
|
||||
{{ end }}
|
||||
|
||||
@@ -138,3 +138,16 @@ spec:
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
---
|
||||
{{ if .Values.frontend.pdb.enabled }}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
spec:
|
||||
maxUnavailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "impress.common.selectorLabels" (list . $component) | nindent 6 }}
|
||||
{{ end }}
|
||||
|
||||
@@ -138,3 +138,16 @@ spec:
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
---
|
||||
{{ if .Values.yProvider.pdb.enabled }}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
spec:
|
||||
maxUnavailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "impress.common.selectorLabels" (list . $component) | nindent 6 }}
|
||||
{{ end }}
|
||||
|
||||
@@ -308,6 +308,9 @@ backend:
|
||||
## @param backend.extraVolumes Additional volumes to mount on the backend.
|
||||
extraVolumes: []
|
||||
|
||||
## @param backend.pdb.enabled Enable pdb on backend
|
||||
pdb:
|
||||
enabled: true
|
||||
|
||||
## @section frontend
|
||||
|
||||
@@ -403,6 +406,10 @@ frontend:
|
||||
## @param frontend.extraVolumes Additional volumes to mount on the frontend.
|
||||
extraVolumes: []
|
||||
|
||||
## @param frontend.pdb.enabled Enable pdb on frontend
|
||||
pdb:
|
||||
enabled: true
|
||||
|
||||
## @section posthog
|
||||
|
||||
posthog:
|
||||
@@ -571,3 +578,7 @@ yProvider:
|
||||
|
||||
## @param yProvider.extraVolumes Additional volumes to mount on the yProvider.
|
||||
extraVolumes: []
|
||||
|
||||
## @param yProvider.pdb.enabled Enable pdb on yProvider
|
||||
pdb:
|
||||
enabled: true
|
||||
|
||||
Reference in New Issue
Block a user