From c0dff25613985760f09e399037440a03027769ab Mon Sep 17 00:00:00 2001 From: Andrew Charneski Date: Sat, 7 Dec 2024 17:04:08 -0500 Subject: [PATCH] input --- webapp/package.json | 2 + webapp/src/components/InputArea.tsx | 246 ++++++++++++++++++++++++++-- 2 files changed, 235 insertions(+), 13 deletions(-) diff --git a/webapp/package.json b/webapp/package.json index 354b0758..a7fc720b 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -23,6 +23,8 @@ "react-redux": "^9.1.2", "react-router-dom": "^7.0.1", "react-scripts": "5.0.1", + "react-markdown": "^9.0.1", + "remark-gfm": "^4.0.0", "styled-components": "^6.1.13", "web-vitals": "^4.2.4" }, diff --git a/webapp/src/components/InputArea.tsx b/webapp/src/components/InputArea.tsx index 74412743..230a7a51 100644 --- a/webapp/src/components/InputArea.tsx +++ b/webapp/src/components/InputArea.tsx @@ -2,6 +2,42 @@ import React, {memo, useCallback, useState} from 'react'; import styled from 'styled-components'; import {useSelector} from 'react-redux'; import {RootState} from '../store'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import Prism from 'prismjs'; +import { + FaBold, + FaItalic, + FaCode, + FaListUl, + FaQuoteRight, + FaLink, + FaHeading, + FaTable, + FaCheckSquare, + FaImage, + FaEye, + FaEdit +} from 'react-icons/fa'; +// Add preview container styles +const PreviewContainer = styled.div` + padding: 0.5rem; + border: 1px solid ${props => props.theme.colors.border}; + border-radius: 0 0 ${props => props.theme.sizing.borderRadius.md} ${props => props.theme.sizing.borderRadius.md}; + background: ${props => props.theme.colors.background}; + min-height: 120px; + max-height: ${({theme}) => theme.sizing.console.maxHeight}; + overflow-y: auto; + pre { + background: ${props => props.theme.colors.surface}; + padding: 1rem; + border-radius: ${props => props.theme.sizing.borderRadius.sm}; + overflow-x: auto; + } + code { + font-family: monospace; + } +`; // Debug logging utility const DEBUG = process.env.NODE_ENV === 'development'; @@ -28,7 +64,6 @@ const InputContainer = styled.div` } border-top: 1px solid ${(props) => props.theme.colors.border}; display: ${({theme, $hide}) => $hide ? 'none' : 'block'}; - max-height: 10vh; position: sticky; bottom: 0; z-index: 10; @@ -44,6 +79,41 @@ const StyledForm = styled.form` gap: 1rem; align-items: flex-start; `; +const EditorToolbar = styled.div` + display: flex; + gap: 0.25rem; + padding: 0.5rem; + flex-wrap: wrap; + background: ${({theme}) => theme.colors.surface}; + border: 1px solid ${({theme}) => theme.colors.border}; + border-bottom: none; + border-radius: ${({theme}) => theme.sizing.borderRadius.md} + ${({theme}) => theme.sizing.borderRadius.md} 0 0; + /* Toolbar sections */ + .toolbar-section { + display: flex; + gap: 0.25rem; + padding: 0 0.5rem; + border-right: 1px solid ${({theme}) => theme.colors.border}; + &:last-child { + border-right: none; + } + } +`; +const ToolbarButton = styled.button` + padding: 0.5rem; + background: transparent; + border: none; + border-radius: ${({theme}) => theme.sizing.borderRadius.sm}; + cursor: pointer; + color: ${({theme}) => theme.colors.text}; + &:hover { + background: ${({theme}) => theme.colors.hover}; + } + &.active { + color: ${({theme}) => theme.colors.primary}; + } +`; const TextArea = styled.textarea` @@ -55,6 +125,7 @@ const TextArea = styled.textarea` resize: vertical; min-height: 40px; max-height: ${({theme}) => theme.sizing.console.maxHeight}; + border-radius: 0 0 ${(props) => props.theme.sizing.borderRadius.md} ${(props) => props.theme.sizing.borderRadius.md}; transition: all 0.3s ease; background: ${({theme}) => theme.colors.background}; @@ -123,11 +194,44 @@ interface InputAreaProps { const InputArea = memo(function InputArea({onSendMessage}: InputAreaProps) { log('Initializing component'); const [message, setMessage] = useState(''); + const [isPreviewMode, setIsPreviewMode] = useState(false); const config = useSelector((state: RootState) => state.config); const messages = useSelector((state: RootState) => state.messages.messages); const [isSubmitting, setIsSubmitting] = useState(false); const textAreaRef = React.useRef(null); const shouldHideInput = config.singleInput && messages.length > 0; + // Add syntax highlighting effect + React.useEffect(() => { + if (isPreviewMode) { + Prism.highlightAll(); + } + }, [isPreviewMode, message]); + const insertMarkdown = useCallback((syntax: string) => { + const textarea = textAreaRef.current; + if (textarea) { + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const selectedText = textarea.value.substring(start, end); + const newText = syntax.replace('$1', selectedText || 'text'); + setMessage(prev => prev.substring(0, start) + newText + prev.substring(end)); + // Set cursor position inside the inserted markdown + setTimeout(() => { + const newCursorPos = start + newText.indexOf(selectedText || 'text'); + textarea.focus(); + textarea.setSelectionRange(newCursorPos, newCursorPos + (selectedText || 'text').length); + }, 0); + } + }, []); + const insertTable = useCallback(() => { + const tableTemplate = ` +| Header 1 | Header 2 | Header 3 | +|----------|----------|----------| +| Cell 1 | Cell 2 | Cell 3 | +| Cell 4 | Cell 5 | Cell 6 | +`.trim() + '\n'; + insertMarkdown(tableTemplate); + }, [insertMarkdown]); + const handleSubmit = useCallback((e: React.FormEvent) => { e.preventDefault(); @@ -183,18 +287,133 @@ const InputArea = memo(function InputArea({onSendMessage}: InputAreaProps) { id="chat-input-container" > -