From fdddd1c56b434ff3aa33919fc547c86f952c48fe Mon Sep 17 00:00:00 2001 From: Birdup <34012548+birdup000@users.noreply.github.com> Date: Fri, 27 Dec 2024 20:32:15 -0600 Subject: [PATCH] checkpoint --- app/components/AIEnhancedRichTextEditor.tsx | 174 +++++---------- .../AIEnhancedRichTextEditor.tsx.new | 209 ++++++++++++++++++ app/components/EditorConfig.ts | 5 + app/components/FormatButton.tsx | 31 +++ app/components/TaskDetailsPanel.tsx | 79 +++++++ app/components/editorUtils.ts | 43 ++++ 6 files changed, 424 insertions(+), 117 deletions(-) create mode 100644 app/components/AIEnhancedRichTextEditor.tsx.new create mode 100644 app/components/FormatButton.tsx create mode 100644 app/components/editorUtils.ts diff --git a/app/components/AIEnhancedRichTextEditor.tsx b/app/components/AIEnhancedRichTextEditor.tsx index 1621de6..09da864 100644 --- a/app/components/AIEnhancedRichTextEditor.tsx +++ b/app/components/AIEnhancedRichTextEditor.tsx @@ -1,8 +1,14 @@ +'use client'; + 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 { createEditor, Descendant, Editor, Element as SlateElement, Node as SlateNode, Text } from 'slate'; +import { Slate, Editable, withReact, ReactEditor, RenderElementProps, RenderLeafProps } from 'slate-react'; +// Using the same type from EditorConfig +import { CustomEditor } from './EditorConfig'; import isHotkey from 'is-hotkey'; import AGiXT from '../utils/agixt'; +import { FormatButton } from './FormatButton'; +import { toggleMark } from './editorUtils'; interface AIEnhancedRichTextEditorProps { placeholder?: string; @@ -39,46 +45,16 @@ const HOTKEYS = { '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 serialize = (nodes: Descendant[]): string => { + return nodes.map(n => SlateNode.string(n)).join('\n'); }; -const isMarkActive = (editor: Editor, format: string) => { - const marks = Editor.marks(editor); - return marks ? marks[format] === true : false; +const deserialize = (text: string): Descendant[] => { + const lines = text.split('\n'); + return lines.map(line => ({ + type: 'paragraph', + children: [{ text: line }], + })); }; const AIEnhancedRichTextEditor: React.FC = ({ @@ -87,14 +63,13 @@ const AIEnhancedRichTextEditor: React.FC = ({ onChange, onCreateTask, }) => { - const editor = useMemo(() => withReact(createEditor()), []); + const editor = useMemo(() => withReact(createEditor()), []) as CustomEditor; 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 { @@ -170,69 +145,35 @@ const AIEnhancedRichTextEditor: React.FC = ({ const content = serialize(value); onChange?.(content); - // Generate AI suggestions when content changes - if (content.length > 50) { // Only trigger for substantial content + if (content.length > 50) { 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 (
-
- - - - - -
- - - -
- - - - -
- +
+ + + + + +
+ + + +
+ + + + +
+ = ({ renderLeaf={renderLeaf} onKeyDown={handleKeyDown} /> - - {/* AI Suggestions Panel */} - {aiSuggestions.length > 0 && ( -
-

AI Suggestions

-
- {aiSuggestions.map((suggestion, index) => ( -
- {suggestion} - -
- ))} + {suggestion} + +
+ ))} +
-
- )} + )} +
); }; -export default AIEnhancedRichTextEditor; \ No newline at end of file +export default React.memo(AIEnhancedRichTextEditor); \ No newline at end of file diff --git a/app/components/AIEnhancedRichTextEditor.tsx.new b/app/components/AIEnhancedRichTextEditor.tsx.new new file mode 100644 index 0000000..2dd807a --- /dev/null +++ b/app/components/AIEnhancedRichTextEditor.tsx.new @@ -0,0 +1,209 @@ +'use client'; + +import React, { useMemo, useCallback, useState } from 'react'; +import { createEditor, Descendant, Editor, Element as SlateElement, Node as SlateNode } from 'slate'; +import { Slate, Editable, withReact, RenderElementProps, RenderLeafProps } from 'slate-react'; +import isHotkey from 'is-hotkey'; +import AGiXT from '../utils/agixt'; +import { FormatButton } from './FormatButton'; +import { toggleMark } from './editorUtils'; + +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 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 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); + + 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); + + if (content.length > 50) { + generateSuggestions(content); + } + }; + + return ( +
    +
    + + + + + +
    + + + +
    + + + + +
    + + + + + + {aiSuggestions.length > 0 && ( +
    +

    AI Suggestions

    +
    + {aiSuggestions.map((suggestion, index) => ( +
    + {suggestion} + +
    + ))} +
    +
    + )} +
    + ); +}; + +export default React.memo(AIEnhancedRichTextEditor); \ No newline at end of file diff --git a/app/components/EditorConfig.ts b/app/components/EditorConfig.ts index c8dd3c8..2b79de9 100644 --- a/app/components/EditorConfig.ts +++ b/app/components/EditorConfig.ts @@ -35,6 +35,11 @@ declare module 'slate' { Element: CustomElement; Text: CustomText; } + + interface SlateElement { + type: string; + children: CustomText[]; + } } // Editor configuration diff --git a/app/components/FormatButton.tsx b/app/components/FormatButton.tsx new file mode 100644 index 0000000..2241115 --- /dev/null +++ b/app/components/FormatButton.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Editor } from 'slate'; +import { useSlate } from 'slate-react'; +import { isBlockActive, isMarkActive, toggleBlock, toggleMark } from './editorUtils'; + +interface FormatButtonProps { + format: string; + icon: string; + isBlock?: boolean; +} + +export const FormatButton: React.FC = ({ format, icon, isBlock = false }) => { + const editor = useSlate(); + const isActive = isBlock ? isBlockActive(editor, format) : isMarkActive(editor, format); + + return ( + + ); +}; \ No newline at end of file diff --git a/app/components/TaskDetailsPanel.tsx b/app/components/TaskDetailsPanel.tsx index 3ca636d..484c4cf 100644 --- a/app/components/TaskDetailsPanel.tsx +++ b/app/components/TaskDetailsPanel.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Task } from '../types/task'; +import NotesEditor from './NotesEditor'; import TaskProgress from './TaskProgress'; import TaskComments from './TaskComments'; import TaskAttachments from './TaskAttachments'; @@ -11,11 +12,14 @@ import TaskRecurrence from './TaskRecurrence'; import TaskHistory from './TaskHistory'; import TaskLabels from './TaskLabels'; import TaskDependencies from './TaskDependencies'; +import AIEnhancedRichTextEditor from './AIEnhancedRichTextEditor'; +import VoiceTaskAssistant from './VoiceTaskAssistant'; interface TaskDetailsPanelProps { task: Task | null; onClose: () => void; onUpdateTask: (task: Task) => void; + onAddTask?: (task: Task) => void; allTasks: Task[]; className?: string; } @@ -351,6 +355,19 @@ const TaskDetailsPanel: React.FC = ({
    +
    +

    + + + + Recurrence +

    + +
    +
    @@ -358,6 +375,68 @@ const TaskDetailsPanel: React.FC = ({
    + +
    +

    + + + + Voice Assistant +

    + +
    + +
    +

    + + + + Notes +

    +
    + { + const updatedNotes = task.notes?.map(note => + note.id === noteId + ? { ...note, linkedTaskIds: [...(note.linkedTaskIds || []), taskId] } + : note + ); + onUpdateTask({ + ...task, + notes: updatedNotes, + activityLog: [ + ...task.activityLog, + { + id: Date.now().toString(), + taskId: task.id, + userId: 'current-user', + action: 'linked_note', + timestamp: new Date(), + }, + ], + }); + }} + /> + onUpdateTask({ + ...task, + description: content, + updatedAt: new Date(), + })} + onCreateTask={(title, description) => { + // Handle task creation from AI suggestions + // This would typically be handled by the parent component + console.log('AI suggested new task:', { title, description }); + }} + /> +
    +
    ); diff --git a/app/components/editorUtils.ts b/app/components/editorUtils.ts new file mode 100644 index 0000000..4b16783 --- /dev/null +++ b/app/components/editorUtils.ts @@ -0,0 +1,43 @@ +import { Editor, Element as SlateElement, Transforms } from 'slate'; + +const LIST_TYPES = new Set(['bullet-list', 'number-list']); + +export 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 as SlateElement).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); + } +}; + +export const toggleMark = (editor: Editor, format: string) => { + const isActive = isMarkActive(editor, format); + if (isActive) { + Editor.removeMark(editor, format); + } else { + Editor.addMark(editor, format, true); + } +}; + +export const isBlockActive = (editor: Editor, format: string) => { + const [match] = Editor.nodes(editor, { + match: n => (n as SlateElement).type === format, + }); + return !!match; +}; + +export const isMarkActive = (editor: Editor, format: string) => { + const marks = Editor.marks(editor); + return marks ? marks[format] === true : false; +}; \ No newline at end of file