From 1dd1a8e04c8df087fd750de750fcae12943f64d9 Mon Sep 17 00:00:00 2001 From: aswanth-c Date: Thu, 29 Aug 2024 12:19:36 -0700 Subject: [PATCH 01/16] WIP Adding ai generation to markdown --- .../mock_srcbook/package-lock.json | 6 + .../web/src/components/ai-prompt-input.tsx | 51 ++++ packages/web/src/components/cells/code.tsx | 53 +--- .../web/src/components/cells/markdown.tsx | 244 ++++++++++++++---- packages/web/src/routes/session.tsx | 2 + 5 files changed, 256 insertions(+), 100 deletions(-) create mode 100644 packages/api/test/srcmd_files/mock_srcbook/package-lock.json create mode 100644 packages/web/src/components/ai-prompt-input.tsx diff --git a/packages/api/test/srcmd_files/mock_srcbook/package-lock.json b/packages/api/test/srcmd_files/mock_srcbook/package-lock.json new file mode 100644 index 00000000..59e161c5 --- /dev/null +++ b/packages/api/test/srcmd_files/mock_srcbook/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "mock_srcbook", + "lockfileVersion": 2, + "requires": true, + "packages": {} +} diff --git a/packages/web/src/components/ai-prompt-input.tsx b/packages/web/src/components/ai-prompt-input.tsx new file mode 100644 index 00000000..2a0cf7ba --- /dev/null +++ b/packages/web/src/components/ai-prompt-input.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Sparkles, MessageCircleWarning, X } from 'lucide-react'; +import TextareaAutosize from 'react-textarea-autosize'; +import { Button } from '@/components/ui/button'; +import AiGenerateTipsDialog from '@/components/ai-generate-tips-dialog'; +import { useNavigate } from 'react-router-dom'; + +interface AiPromptInputProps { + prompt: string; + setPrompt: (prompt: string) => void; + onClose: () => void; + aiEnabled: boolean; +} + +export function AiPromptInput({ prompt, setPrompt, onClose, aiEnabled }: AiPromptInputProps) { + const navigate = useNavigate(); + return ( +
+
+
+ + setPrompt(e.target.value)} + /> +
+
+ + + + +
+
+ {!aiEnabled && ( +
+

API key required

+ navigate('/settings')}> + Settings + +
+ )} +
+ ); +} diff --git a/packages/web/src/components/cells/code.tsx b/packages/web/src/components/cells/code.tsx index 6293b7c7..ffbcf62d 100644 --- a/packages/web/src/components/cells/code.tsx +++ b/packages/web/src/components/cells/code.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import { Dialog, DialogContent } from '@/components/ui/dialog'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import Shortcut from '@/components/keyboard-shortcut'; -import { useNavigate } from 'react-router-dom'; + import { useHotkeys } from 'react-hotkeys-hook'; import CodeMirror, { keymap, Prec } from '@uiw/react-codemirror'; import { javascript } from '@codemirror/lang-javascript'; @@ -43,6 +43,7 @@ import { EditorView } from 'codemirror'; import { EditorState } from '@codemirror/state'; import { unifiedMergeView } from '@codemirror/merge'; import { type Diagnostic, linter } from '@codemirror/lint'; +import { AiPromptInput } from '@/components/ai-prompt-input'; const DEBOUNCE_DELAY = 500; type CellModeType = 'off' | 'generating' | 'reviewing' | 'prompting' | 'fixing'; @@ -354,7 +355,6 @@ function Header(props: { } = props; const { aiEnabled } = useSettings(); - const navigate = useNavigate(); return ( <> @@ -497,49 +497,12 @@ function Header(props: { {['prompting', 'generating'].includes(cellMode) && ( -
-
-
- - setPrompt(e.target.value)} - /> -
-
- - - - -
-
- - {!aiEnabled && ( -
-

API key required

- navigate('/settings')} - > - Settings - -
- )} -
+ setCellMode('off')} + aiEnabled={aiEnabled} + /> )} ); diff --git a/packages/web/src/components/cells/markdown.tsx b/packages/web/src/components/cells/markdown.tsx index 6fbfb1ee..8ed75ba5 100644 --- a/packages/web/src/components/cells/markdown.tsx +++ b/packages/web/src/components/cells/markdown.tsx @@ -3,29 +3,75 @@ import { marked } from 'marked'; import Markdown from 'marked-react'; import CodeMirror, { keymap, Prec, EditorView } from '@uiw/react-codemirror'; import { markdown } from '@codemirror/lang-markdown'; -import { CircleAlert, Trash2, Pencil } from 'lucide-react'; -import { CellType, MarkdownCellType, MarkdownCellUpdateAttrsType } from '@srcbook/shared'; + +import { CircleAlert, Trash2, Pencil, Sparkles, LoaderCircle } from 'lucide-react'; +import { + AiGeneratedCellPayloadType, + CellType, + MarkdownCellType, + MarkdownCellUpdateAttrsType, +} from '@srcbook/shared'; import { cn } from '@/lib/utils'; +import { EditorState } from '@codemirror/state'; import { Button } from '@/components/ui/button'; +import { unifiedMergeView } from '@codemirror/merge'; import DeleteCellWithConfirmation from '@/components/delete-cell-dialog'; import useTheme from '@/components/use-theme'; import { useCells } from '../use-cell'; +import { useSettings } from '@/components/use-settings'; + +import { useHotkeys } from 'react-hotkeys-hook'; +import { useDebouncedCallback } from 'use-debounce'; +import { AiPromptInput } from '../ai-prompt-input'; +import { SessionChannel } from '../../clients/websocket'; +import { SessionType } from '../../types'; +import { stat } from 'fs'; + +const DEBOUNCE_DELAY = 500; +type CellModeType = 'off' | 'generating' | 'reviewing' | 'prompting'; marked.use({ gfm: true }); export default function MarkdownCell(props: { + session: SessionType; + channel: SessionChannel; cell: MarkdownCellType; updateCellOnServer: (cell: MarkdownCellType, attrs: MarkdownCellUpdateAttrsType) => void; onDeleteCell: (cell: CellType) => void; }) { const { codeTheme } = useTheme(); const { updateCell: updateCellOnClient } = useCells(); - const { cell, updateCellOnServer, onDeleteCell } = props; + const { cell, updateCellOnServer, onDeleteCell, channel, session } = props; const defaultState = cell.text ? 'view' : 'edit'; const [status, setStatus] = useState<'edit' | 'view'>(defaultState); const [text, setText] = useState(cell.text); const [error, setError] = useState(null); + const [cellMode, setCellMode] = useState('off'); + const [prompt, setPrompt] = useState(''); + const [newText, setnewText] = useState(''); + const { aiEnabled } = useSettings(); + + useHotkeys( + 'mod+enter', + () => { + if (!prompt) return; + if (cellMode !== 'prompting') return; + if (!aiEnabled) return; + generate(); + }, + { enableOnFormTags: ['textarea'] }, + ); + useHotkeys( + 'escape', + () => { + if (cellMode === 'prompting') { + setCellMode('off'); + setPrompt(''); + } + }, + { enableOnFormTags: ['textarea'] }, + ); useEffect(() => { if (status === 'edit') { setText(cell.text); @@ -51,6 +97,20 @@ export default function MarkdownCell(props: { ]), ); + useEffect(() => { + function callback(payload: AiGeneratedCellPayloadType) { + if (payload.cellId !== cell.id) return; + // We move to the "review" stage of the generation process: + console.log('payload', payload); + setnewText(payload.output); + setCellMode('reviewing'); + } + channel.on('ai:generated', callback); + return () => channel.off('ai:generated', callback); + }, [cell.id, channel]); + + const updateCellOnServerDebounced = useDebouncedCallback(updateCellOnServer, DEBOUNCE_DELAY); + function getValidationError(text: string) { const tokens = marked.lexer(text); const hasH1 = tokens?.some((token) => token.type === 'heading' && token.depth === 1); @@ -64,18 +124,63 @@ export default function MarkdownCell(props: { } function onSave() { - const error = getValidationError(text); + const error = getValidationError(newText); setError(error); if (error === null) { - updateCellOnClient({ ...cell, text }); - updateCellOnServer(cell, { text }); + updateCellOnClient({ ...cell, text: newText }); + updateCellOnServerDebounced(cell, { text: newText }); setStatus('view'); return true; } } + function generate() { + channel.push('ai:generate', { + sessionId: session.id, + cellId: cell.id, + prompt, + }); + setCellMode('generating'); + } + + function onAcceptDiff() { + setText(newText); + updateCellOnClient({ ...cell, text: newText }); + updateCellOnServerDebounced(cell, { text: newText }); + setPrompt(''); + onSave(); + setCellMode('off'); + } + + function onRevertDiff() { + setCellMode('prompting'); + setnewText(''); + } + function DiffEditor({ original, modified }: { original: string; modified: string }) { + const { codeTheme } = useTheme(); + + return ( +
+ +
+ ); + } + return (
- {status === 'view' ? ( -
-
-
-
Markdown
- onDeleteCell(cell)}> - - -
-
+ +
+ {cellMode === 'reviewing' && } + + {['prompting', 'generating'].includes(cellMode) && ( + setCellMode('off')} + aiEnabled={aiEnabled} + /> + )} + + {status === 'view' ? ( +
+ {cell.text}
) : ( <> @@ -120,35 +272,17 @@ export default function MarkdownCell(props: {

{error}

)} -
-
-
-
Markdown
- onDeleteCell(cell)}> - - -
-
- - - -
-
- -
- -
+
+ { + setnewText(newText); + }} + />
)} diff --git a/packages/web/src/routes/session.tsx b/packages/web/src/routes/session.tsx index 04122297..9217f5ce 100644 --- a/packages/web/src/routes/session.tsx +++ b/packages/web/src/routes/session.tsx @@ -247,6 +247,8 @@ function Session(props: { session: SessionType; channel: SessionChannel; config: {cell.type === 'markdown' && ( Date: Thu, 29 Aug 2024 12:28:39 -0700 Subject: [PATCH 02/16] added ai feature to markdown --- packages/web/src/components/cells/markdown.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/web/src/components/cells/markdown.tsx b/packages/web/src/components/cells/markdown.tsx index 8ed75ba5..bca2c7df 100644 --- a/packages/web/src/components/cells/markdown.tsx +++ b/packages/web/src/components/cells/markdown.tsx @@ -192,7 +192,10 @@ export default function MarkdownCell(props: { )} >