This commit is contained in:
Nathan Vasse
2025-02-11 16:51:42 +01:00
parent e26fcdf985
commit cdf157c72b
3 changed files with 141 additions and 50 deletions
@@ -0,0 +1,38 @@
import {
useBlockNoteEditor,
useComponentsContext,
useSelectedBlocks,
} from '@blocknote/react';
import { Text } from '@/components';
export const AIPdfButton = () => {
const editor = useBlockNoteEditor();
const Components = useComponentsContext();
const selectedBlocks = useSelectedBlocks(editor);
if (!Components) {
return null;
}
const show = selectedBlocks.length === 1 && selectedBlocks[0].type === 'file';
if (!show) {
return null;
}
return (
<Components.FormattingToolbar.Button
className="bn-button bn-menu-item"
data-test="ai-actions"
label="AI"
mainTooltip="Chat avec le PDF"
icon={
<Text $isMaterialIcon $size="l">
auto_awesome
</Text>
}
onClick={() => {
console.log('selectedBlocks', selectedBlocks);
}}
/>
);
};
@@ -1,7 +1,6 @@
import { Button, Input, Loader, TextArea } from '@openfun/cunningham-react';
import { Button, Input, Loader } 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';
@@ -49,7 +48,7 @@ export const SuggestionButton = styled.button`
`;
export const AiButton = ({ doc }: { doc: Doc }) => {
const [isOpen, setIsOpen] = useState(true);
const [isOpen, setIsOpen] = useState(false);
return (
<>
@@ -65,7 +64,7 @@ export const AiButton = ({ doc }: { doc: Doc }) => {
`}
>
<AIButtonEl
aria-label="Ask anything to our AI"
aria-label="Posez une question à Albert à propos de ce document"
onClick={() => setIsOpen(true)}
/>
</Box>
@@ -90,22 +89,16 @@ const AiChat = (props: { isOpen: boolean; onClose: () => void; doc: Doc }) => {
return null;
}
const submit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const newPrompt = async (prompt: string) => {
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({
@@ -116,7 +109,7 @@ const AiChat = (props: { isOpen: boolean; onClose: () => void; doc: Doc }) => {
}),
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const data = await response.json();
const data = (await response.json()) as string;
console.log('response', data);
setMessages((messages) => [
@@ -126,6 +119,12 @@ const AiChat = (props: { isOpen: boolean; onClose: () => void; doc: Doc }) => {
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"
@@ -179,13 +178,23 @@ const AiChat = (props: { isOpen: boolean; onClose: () => void; doc: Doc }) => {
Suggestions
</Text>
<Box>
<SuggestionButton>
<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>
<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">surcette page</span>
Poser des questions <span className="sub">sur cette page</span>
</SuggestionButton>
</Box>
</Box>
@@ -201,37 +210,16 @@ const AiChat = (props: { isOpen: boolean; onClose: () => void; doc: Doc }) => {
mask-image: linear-gradient(black calc(100% - 32px), transparent calc(100% - 4px));
padding-bottom: 32px;
`}
aria-live="polite"
>
{messages.map((message, index) => {
if (message.role === 'user') {
return (
<Box
key={index}
$css={`
border-radius: 16px;
padding: 6px 14px;
background-color: rgba(55, 53, 47, 0.04);
${message.role === 'user' ? 'margin-left: auto;' : ''}
`}
>
{message.content}
</Box>
);
}
return (
<Box
key={index}
dangerouslySetInnerHTML={{
__html: marked.parse(message.content),
}}
></Box>
);
})}
{messages.map((message, index) => (
<Message key={index} message={message} />
))}
{(isLoading || false) && (
<Box $display="flex" $direction="row" $align="center" $gap="0.5rem">
<Loader size="small" />
Thinking ...
Albert réfléchit ...
</Box>
)}
</Box>
@@ -251,3 +239,62 @@ const AiChat = (props: { isOpen: boolean; onClose: () => void; doc: Doc }) => {
</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>
);
};
@@ -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>
);
},
[],
);