Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(editor): add ai experiments #4348

Draft
wants to merge 31 commits into
base: staging
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b1c1a12
feat(plugin-ai-generation): init new plugin
elbotho Dec 10, 2024
9a3fb5b
refactor(editor): extract decoder from paste hack
elbotho Dec 10, 2024
ae8b850
feat(plugin-ai-generation): add mocked logic
elbotho Dec 10, 2024
2a34303
refactor(editor): add isProduction to serlo context
elbotho Dec 10, 2024
0095979
feat(plugin-ai-generation): add ai change tool in toolbar menu (mocked)
elbotho Dec 10, 2024
dc2aef3
chore(plugin-ai-generation): fix typo in filename
elbotho Dec 16, 2024
552e8a8
refactor(ai): fix typo
hugotiburtino Dec 20, 2024
beb6a05
feat(ai): use real ai cresponse for change content
hugotiburtino Jan 9, 2025
42426d0
refactor(ai): cuse dev env backend instead of local
hugotiburtino Jan 10, 2025
f3e280a
feat(ai): generate text per ai, though not aware of context
hugotiburtino Jan 14, 2025
e7f08b4
Merge branch 'staging' into feat/editor-ai-experiments
elbotho Jan 15, 2025
d2793de
fix(ai-generation): add icon as component as well
elbotho Jan 15, 2025
fc7963b
refactor(ai-generation): check parent plugin before fetch
elbotho Jan 15, 2025
c53b56f
refactor(ai-generation): add extraction of context
elbotho Jan 15, 2025
c727da9
fix: prettier
elbotho Jan 15, 2025
2db39ba
Merge pull request #4403 from serlo/feat/editor-ai-extract-context
hugotiburtino Jan 15, 2025
a88a3d5
Merge branch 'staging' into feat/editor-ai-experiments
elbotho Jan 28, 2025
27fd683
refactor(ai): add exercise to supported plugins in decoder
hugotiburtino Jan 30, 2025
e2d579c
refactor(ai): use just one way to decode response
hugotiburtino Jan 30, 2025
28753a9
merge staging
hejtful Jan 30, 2025
d92d71c
fix(editor): leftover from merge
hejtful Jan 30, 2025
2bc4b99
fix(ai-generation): dependency cycle
hejtful Jan 30, 2025
b4dfe73
chore: run yarn
elbotho Jan 31, 2025
bdf2601
fix: add ai-generation plugin to menu again
elbotho Jan 31, 2025
003a0d2
refactor(ai): add loading state to generate
hugotiburtino Feb 2, 2025
8713c25
refactor: fix import order
hugotiburtino Feb 3, 2025
8685fb3
fix(ai-generation): import svg as component from editor
elbotho Feb 3, 2025
96ed7b1
feat(ai-generation): improve loading state
elbotho Feb 3, 2025
43bae5e
feat(ai-generation): allow closing of modal / canceling
elbotho Feb 3, 2025
3a1917e
feat(ai-generation): differentiate change and generate ui
elbotho Feb 3, 2025
80c0df0
refactor(ai-generation): improve error handling, recover prompt
elbotho Feb 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
4 changes: 4 additions & 0 deletions apps/web/src/pages/___editor_preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { parseDocumentString } from '@/helper/parse-document-string'
import { renderedPageNoHooks } from '@/helper/rendered-page'
import { showToastNotice } from '@/helper/show-toast-notice'
import { EditorRenderer } from '@/serlo-editor-integration/editor-renderer'
import { extraSerloPlugins } from '@/serlo-editor-integration/extra-serlo-plugins'
import { extraSerloRenderers } from '@/serlo-editor-integration/extra-serlo-renderers'

const Editor = dynamic(
() => import('@editor/package').then((mod) => mod.SerloEditor),
Expand Down Expand Up @@ -76,6 +78,8 @@ function Content() {
if (stringifiedNewState === previewState) return
void debouncedSetState(stringifiedNewState)
}}
extraSerloPlugins={extraSerloPlugins}
extraSerloRenderers={extraSerloRenderers}
>
{({ element }) => element}
</Editor>
Expand Down
4 changes: 4 additions & 0 deletions packages/editor/src/core/hooks/use-is-serlo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ export function useIsSerlo(): boolean {
const isSerlo = useContext(EditorMetaContext).editorVariant === 'serlo-org'
return isSerlo ?? false
}

export function useIsNextjsProduction(): boolean {
return process.env.NEXT_PUBLIC_ENV === 'production'
}
12 changes: 11 additions & 1 deletion packages/editor/src/editor-integration/create-plugins.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { SupportedLanguage } from '@editor/package'
import type { EditorPlugin, StringStateType } from '@editor/plugin'
import { aiGenerationPlugin } from '@editor/plugins/ai-generation'
import { anchorPlugin } from '@editor/plugins/anchor'
import { articlePlugin } from '@editor/plugins/article'
import { createBlanksExercisePlugin } from '@editor/plugins/blanks-exercise'
Expand Down Expand Up @@ -65,7 +66,8 @@ export function createPlugins(
plugins: (EditorPluginType | TemplatePluginType)[],
testingSecret?: string | null,
language: SupportedLanguage = 'de',
extraSerloPlugins?: ExtraSerloPlugins
extraSerloPlugins?: ExtraSerloPlugins,
isSerloProduction: boolean = false
) {
const allPlugins = [
{
Expand Down Expand Up @@ -232,6 +234,14 @@ export function createPlugins(
type: EditorPluginType.Anchor,
plugin: anchorPlugin,
},
...(isSerloProduction
? []
: [
{
type: EditorPluginType.AiGeneration,
plugin: aiGenerationPlugin,
},
]),
]
: []),
]
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
export function AiGenerationIcon() {
return (
<svg viewBox="0 0 90 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="90" height="60" rx="4.2" fill="#FFF5E9" />
<rect
x="12.75"
y="5.56641"
width="64.3923"
height="3.95769"
fill="black"
fillOpacity="0.08"
/>
<rect
width="64.5"
height="31.95"
transform="translate(12.75 14.0234)"
fill="#FFDDB2"
/>
<path
d="M52.1075 25.9995L52.8975 24.2495L54.6475 23.4595C55.0375 23.2795 55.0375 22.7295 54.6475 22.5495L52.8975 21.7595L52.1075 19.9995C51.9275 19.6095 51.3775 19.6095 51.1975 19.9995L50.4075 21.7495L48.6475 22.5395C48.2575 22.7195 48.2575 23.2695 48.6475 23.4495L50.3975 24.2395L51.1875 25.9995C51.3675 26.3895 51.9275 26.3895 52.1075 25.9995ZM44.1475 27.4995L42.5575 23.9995C42.2075 23.2195 41.0875 23.2195 40.7375 23.9995L39.1475 27.4995L35.6475 29.0895C34.8675 29.4495 34.8675 30.5595 35.6475 30.9095L39.1475 32.4995L40.7375 35.9995C41.0975 36.7795 42.2075 36.7795 42.5575 35.9995L44.1475 32.4995L47.6475 30.9095C48.4275 30.5495 48.4275 29.4395 47.6475 29.0895L44.1475 27.4995ZM51.1875 33.9995L50.3975 35.7495L48.6475 36.5395C48.2575 36.7195 48.2575 37.2695 48.6475 37.4495L50.3975 38.2395L51.1875 39.9995C51.3675 40.3895 51.9175 40.3895 52.0975 39.9995L52.8875 38.2495L54.6475 37.4595C55.0375 37.2795 55.0375 36.7295 54.6475 36.5495L52.8975 35.7595L52.1075 33.9995C51.9275 33.6095 51.3675 33.6095 51.1875 33.9995Z"
fill="url(#paint0_linear_11114_34625)"
/>
<rect
x="12.75"
y="50.4727"
width="64.3923"
height="3.95769"
fill="black"
fillOpacity="0.08"
/>
<defs>
<linearGradient
id="paint0_linear_11114_34625"
x1="35"
y1="30"
x2="58.5"
y2="30"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#D4B3FF" />
<stop offset="1" stopColor="#9747FF" />
</linearGradient>
</defs>
</svg>
)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import {
useIsNextjsProduction,
useIsSerlo,
} from '@editor/core/hooks/use-is-serlo'
import { useEditStrings } from '@editor/i18n/edit-strings-provider'
import { AiChangePluginTool } from '@editor/plugins/ai-generation/plugin-change-tool/ai-change-plugin-tool'
import {
insertPluginChildAfter,
removePluginChild,
Expand Down Expand Up @@ -33,6 +38,10 @@ export function PluginDefaultTools({ pluginId }: PluginDefaultToolsProps) {
[pluginId, store]
)

const isSerlo = useIsSerlo()
const isSerloProduction = useIsNextjsProduction()
const showAiTools = isSerlo && !isSerloProduction

const handleDuplicatePlugin = useCallback(() => {
const parent = selectChildTreeOfParent(store.getState(), pluginId)
if (!parent) return
Expand Down Expand Up @@ -78,7 +87,9 @@ export function PluginDefaultTools({ pluginId }: PluginDefaultToolsProps) {
<>
{hasRowsParent ? (
<>
{showAiTools ? <AiChangePluginTool pluginId={pluginId} /> : null}
<DropdownButton
separatorTop={showAiTools}
onClick={handleDuplicatePlugin}
label={pluginStrings.rows.duplicate}
icon={faClone}
Expand Down
16 changes: 16 additions & 0 deletions packages/editor/src/i18n/strings/de/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ export const editStrings = {
},
},
plugins: {
aiGeneration: {
title: 'Inhalt per AI generieren (Test)',
modalTitle: 'Was soll generiert werden?',
modalChangeTitle: 'Was soll geändert werden?',
placeholder:
'z.B. "Wiederhole den Satz des Pythagoras mit einer interaktiven Verständnisfrage am Ende',
changePlaceholder:
'Beschreibe, was du ändern möchtest. Beispiel: "Überarbeite und kürze die Erklärung"',
buttonText: 'Inhalt generieren',
changeButtonText: 'Inhalt überarbeiten',
menuText: 'Aktuelles Plugin per AI überarbeiten',
generating: 'Inhalt wird bearbeitet…',
},
anchor: {
title: 'Sprungmarke',
description: 'Füge eine Sprungmarke innerhalb deines Inhalts hinzu.',
Expand Down Expand Up @@ -532,4 +545,7 @@ export const editStrings = {
createdAt: 'Zeitstempel',
ready: 'Bereit zum Speichern?',
},
aiFeatures: {
pluginToolReworkWithAi: 'Aktuelles Plugin per AI überarbeiten (Test)',
},
}
16 changes: 16 additions & 0 deletions packages/editor/src/i18n/strings/en/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ export const editStrings = {
},
},
plugins: {
aiGeneration: {
title: 'Create content via AI (Test)',
modalTitle: 'What should be generated?',
modalChangeTitle: 'What should be changed?',
placeholder:
'Describe what you want to generate. E.g. "Create an explanation for the quadratic formula with an exercise at the end."',
changePlaceholder:
'Describe what you want to change. E.g. "Revise and shorten the explanation"',
buttonText: 'Generate content',
changeButtonText: 'Change content',
menuText: 'Revise current plugin via AI',
generating: 'Working on content...',
},
anchor: {
title: 'Anchor',
description: 'Insert an anchor.',
Expand Down Expand Up @@ -516,4 +529,7 @@ export const editStrings = {
createdAt: 'when?',
ready: 'Ready to save?',
},
aiFeatures: {
pluginToolReworkWithAi: 'Revise current plugin via AI (Test)',
},
}
1 change: 1 addition & 0 deletions packages/editor/src/package/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { SupportedLanguage } from '@editor/types/language-data'
import { TemplatePluginType } from '@editor/types/template-plugin-type'

export const defaultPlugins = [
EditorPluginType.AiGeneration,
EditorPluginType.Text,
EditorPluginType.Image,
EditorPluginType.Video,
Expand Down
6 changes: 5 additions & 1 deletion packages/editor/src/package/editor.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Editor, type EditorProps } from '@editor/core'
import { EditorMetaContext } from '@editor/core/contexts/editor-meta-context'
import { useIsNextjsProduction } from '@editor/core/hooks/use-is-serlo'
import { type GetDocument } from '@editor/core/types'
import {
createPlugins,
Expand Down Expand Up @@ -51,6 +52,8 @@ export interface SerloEditorProps {

/** For exporting the editor */
export function SerloEditor(props: SerloEditorProps) {
const isSerloProduction = useIsNextjsProduction()

const {
children,
editorVariant,
Expand Down Expand Up @@ -85,7 +88,8 @@ export function SerloEditor(props: SerloEditorProps) {
plugins,
_testingSecret,
language,
extraSerloPlugins
extraSerloPlugins,
isSerloProduction
)
editorPlugins.init(allPlugins)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useEditStrings } from '@editor/i18n/edit-strings-provider'
import { cn } from '@editor/utils/cn'
import { useEffect, useState } from 'react'

import { Sparkles } from './sparkles'

export function PromptForm({
initialPrompt,
type,
onSubmit,
}: {
initialPrompt?: string
type: 'generation' | 'change'
onSubmit: (prompt: string) => void
}) {
const aiStrings = useEditStrings().plugins.aiGeneration
const [prompt, setPrompt] = useState('')
const [isLoading, setIsLoading] = useState(false)

useEffect(() => {
if (initialPrompt && initialPrompt.length > 0) {
setPrompt(initialPrompt)
setIsLoading(false)
}
}, [initialPrompt])

return isLoading ? (
<div className="mx-side flex animate-pulse items-center gap-4 text-xl">
<div className="h-12 w-12 ">
<Sparkles />
</div>
{aiStrings.generating}
</div>
) : (
<form>
<textarea
className={cn(`ml-side w-[calc(100%-32px)] rounded-xl border-2 border-editor-primary-100
bg-editor-primary-100 px-2.5 py-2 text-almost-black
focus:border-editor-primary focus:outline-none`)}
value={prompt}
rows={5}
placeholder={
type === 'generation'
? aiStrings.placeholder
: aiStrings.changePlaceholder
}
onChange={(e) => setPrompt(e.target.value)}
/>
<button
type="submit"
className="serlo-button-edit-primary mx-side mt-4 px-4"
onClick={(e) => {
e.preventDefault()
onSubmit(prompt)
setPrompt('')
setIsLoading(true)
}}
>
{type === 'generation'
? aiStrings.buttonText
: aiStrings.changeButtonText}
</button>
</form>
)
}
29 changes: 29 additions & 0 deletions packages/editor/src/plugins/ai-generation/components/sparkles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export function Sparkles() {
return (
<svg
width="100%"
height="100%"
viewBox="0 0 231 231"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M183.898 77.0009L191.501 60.1571L208.345 52.5534C212.099 50.8209 212.099 45.5271 208.345 43.7946L191.501 36.1909L183.898 19.2509C182.165 15.4971 176.871 15.4971 175.139 19.2509L167.535 36.0946L150.595 43.6984C146.841 45.4309 146.841 50.7246 150.595 52.4571L167.439 60.0609L175.043 77.0009C176.775 80.7546 182.165 80.7546 183.898 77.0009ZM107.283 91.4384L91.979 57.7509C88.6102 50.2434 77.8302 50.2434 74.4615 57.7509L59.1577 91.4384L25.4702 106.742C17.9627 110.207 17.9627 120.891 25.4702 124.26L59.1577 139.563L74.4615 173.251C77.9265 180.758 88.6102 180.758 91.979 173.251L107.283 139.563L140.97 124.26C148.478 120.795 148.478 110.111 140.97 106.742L107.283 91.4384ZM175.043 154.001L167.439 170.845L150.595 178.448C146.841 180.181 146.841 185.475 150.595 187.207L167.439 194.811L175.043 211.751C176.775 215.505 182.069 215.505 183.801 211.751L191.405 194.907L208.345 187.303C212.099 185.571 212.099 180.277 208.345 178.545L191.501 170.941L183.898 154.001C182.165 150.247 176.775 150.247 175.043 154.001Z"
fill="url(#paint0_linear_6744_14969)"
/>
<defs>
<linearGradient
id="paint0_linear_6744_14969"
x1="52.9373"
y1="33.6929"
x2="211.75"
y2="255.068"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#0076B9" />
<stop offset="0.822917" stopColor="#2FCEB1" />
</linearGradient>
</defs>
</svg>
)
}
32 changes: 32 additions & 0 deletions packages/editor/src/plugins/ai-generation/decoder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { EditorPluginType } from '@editor/types/editor-plugin-type'
import * as t from 'io-ts'

/*
* Simple validation of editor state
*/
export const StateDecoder = t.strict({
plugin: t.literal(EditorPluginType.Rows),
state: t.array(
t.strict({
plugin: t.union([
t.literal(EditorPluginType.Article),
t.literal(EditorPluginType.ArticleIntroduction),
t.literal(EditorPluginType.Geogebra),
t.literal(EditorPluginType.Anchor),
t.literal(EditorPluginType.Video),
t.literal(EditorPluginType.Audio),
t.literal(EditorPluginType.SerloTable),
t.literal(EditorPluginType.Highlight),
t.literal(EditorPluginType.Injection),
t.literal(EditorPluginType.Multimedia),
t.literal(EditorPluginType.Spoiler),
t.literal(EditorPluginType.Box),
t.literal(EditorPluginType.Image),
t.literal(EditorPluginType.Text),
t.literal(EditorPluginType.Equations),
t.literal(EditorPluginType.Exercise),
]),
state: t.unknown,
})
),
})
Loading