diff --git a/docker-compose.yml b/docker-compose.yml index 74c90c08..235bbdd7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index e9616aff..e6a51944 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -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//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"], diff --git a/src/backend/core/services/ai_services.py b/src/backend/core/services/ai_services.py index 102e8689..e00761b8 100644 --- a/src/backend/core/services/ai_services.py +++ b/src/backend/core/services/ai_services.py @@ -55,6 +55,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='meta-llama/Llama-3.1-8B-Instruct', + 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( diff --git a/src/frontend/apps/impress/package.json b/src/frontend/apps/impress/package.json index e36903db..e8400745 100644 --- a/src/frontend/apps/impress/package.json +++ b/src/frontend/apps/impress/package.json @@ -34,6 +34,7 @@ "idb": "8.0.2", "lodash": "4.17.21", "luxon": "3.5.0", + "marked": "^15.0.7", "next": "15.1.6", "posthog-js": "1.215.6", "react": "*", diff --git a/src/frontend/apps/impress/public/assets/ia_baguette.png b/src/frontend/apps/impress/public/assets/ia_baguette.png new file mode 100644 index 00000000..c5c2e0f3 Binary files /dev/null and b/src/frontend/apps/impress/public/assets/ia_baguette.png differ diff --git a/src/frontend/apps/impress/public/assets/ia_baguette_question_mark.png b/src/frontend/apps/impress/public/assets/ia_baguette_question_mark.png new file mode 100644 index 00000000..45d83050 Binary files /dev/null and b/src/frontend/apps/impress/public/assets/ia_baguette_question_mark.png differ diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/Ai/AiButton.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/Ai/AiButton.tsx new file mode 100644 index 00000000..6d87781a --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/Ai/AiButton.tsx @@ -0,0 +1,253 @@ +import { Button, Input, Loader, TextArea } from '@openfun/cunningham-react'; +import { marked } from 'marked'; +import { useState } from 'react'; +import { ButtonProps } from 'react-aria-components'; +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 [isOpen, setIsOpen] = useState(true); + + return ( + <> + + setIsOpen(true)} + /> + + + 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([]); + + if (!props.isOpen) { + return null; + } + + const submit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!editor) { + return; + } + + setIsLoading(true); + setMessages([...messages, { role: 'user', content: prompt }]); + setPrompt(''); // Clear the prompt after submitting the form + + const editorContentFormatted = await editor.blocksToMarkdownLossy(); + + console.log('submit', e, prompt); + console.log('editorContentFormatted', editorContentFormatted); + + 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(); + + console.log('response', data); + setMessages((messages) => [ + ...messages, + { role: 'assistant', content: data }, + ]); + setIsLoading(false); + }; + + return ( + + + + {messages.length == 0 ? '' : 'Demander à Albert'} + +