From b973fc411380aec2f568635e218a634da1088fed Mon Sep 17 00:00:00 2001 From: Birdup <34012548+birdup000@users.noreply.github.com> Date: Fri, 27 Dec 2024 19:01:49 -0600 Subject: [PATCH] checkpoint --- app/components/AIEnhancedRichTextEditor.tsx | 271 ++++++++++++++++++++ app/components/NotesEditor.tsx | 34 ++- 2 files changed, 303 insertions(+), 2 deletions(-) create mode 100644 app/components/AIEnhancedRichTextEditor.tsx diff --git a/app/components/AIEnhancedRichTextEditor.tsx b/app/components/AIEnhancedRichTextEditor.tsx new file mode 100644 index 0000000..1621de6 --- /dev/null +++ b/app/components/AIEnhancedRichTextEditor.tsx @@ -0,0 +1,271 @@ +import React, { useMemo, useCallback, useState } from 'react'; +import { createEditor, Descendant, Editor, Element as SlateElement, Text, Node as SlateNode, Transforms } from 'slate'; +import { Slate, Editable, withReact, RenderElementProps, RenderLeafProps, useSlate } from 'slate-react'; +import isHotkey from 'is-hotkey'; +import AGiXT from '../utils/agixt'; + +interface AIEnhancedRichTextEditorProps { + placeholder?: string; + initialContent?: string; + onChange?: (content: string) => void; + onCreateTask?: (title: string, description: string) => void; +} + +const ELEMENT_TYPES = { + paragraph: 'paragraph', + heading1: 'heading1', + heading2: 'heading2', + heading3: 'heading3', + bulletList: 'bullet-list', + numberList: 'number-list', + listItem: 'list-item', + blockquote: 'blockquote', + codeBlock: 'code-block', +} as const; + +const MARK_TYPES = { + bold: 'bold', + italic: 'italic', + underline: 'underline', + code: 'code', + strikethrough: 'strikethrough', +} as const; + +const HOTKEYS = { + 'mod+b': 'bold', + 'mod+i': 'italic', + 'mod+u': 'underline', + 'mod+`': 'code', + 'mod+shift+s': 'strikethrough', +}; + +const LIST_TYPES = new Set(['bullet-list', 'number-list']); + +const toggleBlock = (editor: Editor, format: string) => { + const isActive = isBlockActive(editor, format); + const isList = LIST_TYPES.has(format); + + Transforms.unwrapNodes(editor, { + match: n => LIST_TYPES.has(n.type as string), + split: true, + }); + + Transforms.setNodes(editor, { + type: isActive ? 'paragraph' : isList ? 'list-item' : format, + }); + + if (!isActive && isList) { + const block = { type: format, children: [] }; + Transforms.wrapNodes(editor, block); + } +}; + +const toggleMark = (editor: Editor, format: string) => { + const isActive = isMarkActive(editor, format); + if (isActive) { + Editor.removeMark(editor, format); + } else { + Editor.addMark(editor, format, true); + } +}; + +const isBlockActive = (editor: Editor, format: string) => { + const [match] = Editor.nodes(editor, { + match: n => n.type === format, + }); + return !!match; +}; + +const isMarkActive = (editor: Editor, format: string) => { + const marks = Editor.marks(editor); + return marks ? marks[format] === true : false; +}; + +const AIEnhancedRichTextEditor: React.FC = ({ + placeholder = 'Start typing...', + initialContent = '', + onChange, + onCreateTask, +}) => { + const editor = useMemo(() => withReact(createEditor()), []); + const [editorValue, setEditorValue] = useState(() => + deserialize(initialContent) + ); + const [aiSuggestions, setAiSuggestions] = useState([]); + const [isProcessing, setIsProcessing] = useState(false); + + // AI-powered suggestions + const generateSuggestions = async (content: string) => { + setIsProcessing(true); + try { + const agixt = new AGiXT(); + const response = await agixt.generate({ + prompt: `Analyze the following note content and suggest relevant tasks, tags, or related information:\n\n${content}`, + commands: ['suggest_tasks', 'suggest_tags'], + }); + setAiSuggestions(response.suggestions || []); + } catch (error) { + console.error('Error generating AI suggestions:', error); + } finally { + setIsProcessing(false); + } + }; + + const renderElement = useCallback((props: RenderElementProps) => { + switch (props.element.type) { + case ELEMENT_TYPES.heading1: + return

{props.children}

; + case ELEMENT_TYPES.heading2: + return

{props.children}

; + case ELEMENT_TYPES.heading3: + return

{props.children}

; + case ELEMENT_TYPES.bulletList: + return
    {props.children}
; + case ELEMENT_TYPES.numberList: + return
    {props.children}
; + case ELEMENT_TYPES.listItem: + return
  • {props.children}
  • ; + case ELEMENT_TYPES.blockquote: + return
    {props.children}
    ; + case ELEMENT_TYPES.codeBlock: + return
    {props.children}
    ; + default: + return

    {props.children}

    ; + } + }, []); + + const renderLeaf = useCallback((props: RenderLeafProps) => { + let { attributes, children, leaf } = props; + + if (leaf.bold) { + children = {children}; + } + if (leaf.italic) { + children = {children}; + } + if (leaf.underline) { + children = {children}; + } + if (leaf.code) { + children = {children}; + } + if (leaf.strikethrough) { + children = {children}; + } + + return {children}; + }, []); + + const handleKeyDown = useCallback((event: React.KeyboardEvent) => { + for (const hotkey in HOTKEYS) { + if (isHotkey(hotkey, event)) { + event.preventDefault(); + toggleMark(editor, HOTKEYS[hotkey as keyof typeof HOTKEYS]); + } + } + }, [editor]); + + const handleChange = (value: Descendant[]) => { + setEditorValue(value); + const content = serialize(value); + onChange?.(content); + + // Generate AI suggestions when content changes + if (content.length > 50) { // Only trigger for substantial content + generateSuggestions(content); + } + }; + + const serialize = (nodes: Descendant[]): string => { + return nodes.map(n => SlateNode.string(n)).join('\n'); + }; + + const deserialize = (text: string): Descendant[] => { + const lines = text.split('\n'); + return lines.map(line => ({ + type: 'paragraph', + children: [{ text: line }], + })); + }; + + const FormatButton = ({ format, icon, isBlock = false }) => { + const editor = useSlate(); + const isActive = isBlock ? isBlockActive(editor, format) : isMarkActive(editor, format); + + return ( + + ); + }; + + return ( +
    +
    + + + + + +
    + + + +
    + + + + +
    + + + + + + {/* AI Suggestions Panel */} + {aiSuggestions.length > 0 && ( +
    +

    AI Suggestions

    +
    + {aiSuggestions.map((suggestion, index) => ( +
    + {suggestion} + +
    + ))} +
    +
    + )} +
    + ); +}; + +export default AIEnhancedRichTextEditor; \ No newline at end of file diff --git a/app/components/NotesEditor.tsx b/app/components/NotesEditor.tsx index 69a39bd..e140802 100644 --- a/app/components/NotesEditor.tsx +++ b/app/components/NotesEditor.tsx @@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect } from 'react'; import IntegrationButton from './IntegrationButton'; -import RichTextEditor from './RichTextEditor'; +import AIEnhancedRichTextEditor from './AIEnhancedRichTextEditor'; import SubNotesList from './SubNotesList'; import TaskSelectDialog from './TaskSelectDialog'; import { Task } from '../types/task'; @@ -406,7 +406,7 @@ const NotesEditor: React.FC = ({ initialNotes = [], tasks = []
    - updateNote({ @@ -415,6 +415,36 @@ const NotesEditor: React.FC = ({ initialNotes = [], tasks = [] updatedAt: new Date(), }) } + onCreateTask={(title, description) => { + // Create a new task from the suggestion + const newTask = { + id: crypto.randomUUID(), + title, + description, + status: 'todo', + priority: 'medium', + progress: 0, + createdAt: new Date(), + updatedAt: new Date(), + linkedNoteIds: [selectedNote.id], + activityLog: [{ + id: Date.now().toString(), + taskId: null, // Will be set after task creation + userId: 'current-user', + action: 'created', + timestamp: new Date(), + }], + }; + + // Update the note with the new task link + const updatedNote = { + ...selectedNote, + linkedTaskIds: [...(selectedNote.linkedTaskIds || []), newTask.id], + updatedAt: new Date(), + }; + + updateNote(updatedNote); + }} /> ) : (