diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4ad84cf..898610c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -25,7 +25,7 @@ import { VectorVisualizer } from './VectorVisualizer'; import { Controls } from './Controls'; import { Conversation, ConversationHandle } from './Conversation'; import { ChatQuery, QueryOptions } from './ChatQuery'; -import { Scrollable } from './AutoScroll'; +import { Scrollable } from './Scrollable'; import { BackstoryTab } from './BackstoryTab'; import './App.css'; @@ -177,26 +177,23 @@ const App = () => { iconPosition: "start" }, children: ( - - - + ref={chatRef} + {...{ + type: "chat", + prompt: "What would you like to know about James?", + resetLabel: "chat", + sessionId, + connectionBase, + setSnack, + preamble: backstoryPreamble, + defaultPrompts: backstoryQuestions + }} + /> ) }, { label: "Resume Builder", @@ -323,7 +320,7 @@ const App = () => { useEffect(() => { const url = new URL(window.location.href); const pathParts = url.pathname.split('/').filter(Boolean); // [path, sessionId] - console.log(tabs); + if (pathParts.length < 1) { console.log("No session id or path -- creating new session"); fetchSession(); diff --git a/frontend/src/AutoScroll.tsx b/frontend/src/AutoScroll.tsx deleted file mode 100644 index 44d5de5..0000000 --- a/frontend/src/AutoScroll.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import { useEffect, useRef, RefObject } from 'react'; -import Box from '@mui/material/Box'; -import { SxProps, Theme } from '@mui/material'; - -/** - * Hook that automatically scrolls a container to the bottom when content changes - * or when the container is resized, but only if the user is already near the bottom. - * - * @param threshold - Distance from bottom (px) to consider "near bottom" (default: 100) - * @param smooth - Whether to use smooth scrolling (default: true) - * @returns Ref to attach to the scrollable container - */ -const useAutoScrollToBottom = ( - threshold: number = 0.33, // Percentage of viewport to trigger threshold - smooth: boolean = true -): RefObject => { - const containerRef = useRef(null); - const lastScrollTop = useRef(0); - const scrollTimeout = useRef(null); - - useEffect(() => { - const container = containerRef.current; - if (!container) { - // console.log("No ref"); - return; - } - const lastScrollHeight = container.scrollHeight; - var isUserScrollingUp = false; - - // Function to check if we should scroll to bottom - const checkAndScrollToBottom = (priorScrollHeight?: number | undefined): void => { - if (!container) return; - - const scrollHeight = (priorScrollHeight !== undefined) ? priorScrollHeight : container.scrollHeight; - - // Only auto-scroll if the user is near the bottom and not actively scrolling up - const isNearBottom: boolean = - scrollHeight - container.scrollTop - container.clientHeight <= container.clientHeight * threshold; - - if (isNearBottom && !isUserScrollingUp) { - // console.log('Scrolling', { - // isNearBottom, - // isUserScrollingUp, - // scrollHeightToUser: scrollHeight, - // scrollHeight: container.scrollHeight, - // scrollTop: container.scrollTop, - // clientHeight: container.clientHeight, - // threshold, - // delta: container.scrollHeight - container.scrollTop - container.clientHeight - // }); - container.scrollTo({ - top: container.scrollHeight, - behavior: smooth ? 'smooth' : 'auto' - }); - } else { - // console.log('Not scrolling', { - // isNearBottom, - // isUserScrollingUp, - // scrollHeight: container.scrollHeight, - // scrollTop: container.scrollTop, - // clientHeight: container.clientHeight, - // threshold, - // delta: container.scrollHeight - container.scrollTop - container.clientHeight - // }); - } - }; - - // Set up ResizeObserver to detect content size changes - const resizeObserver = new ResizeObserver(() => { - const container = containerRef.current; - if (!container) { - return; - } - - checkAndScrollToBottom(lastScrollHeight); - }); - - // Observe the container and its children - resizeObserver.observe(container); - Array.from(container.children).forEach((child) => { - resizeObserver.observe(child); - }); - - // Track user scrolling behavior - const handleScroll = (): void => { - if (!container) { - // console.log("No ref in handleScroll"); - return; - } - - // Clear existing timeout - if (scrollTimeout.current) { - clearTimeout(scrollTimeout.current); - } - - // Determine scroll direction - const currentScrollTop = container.scrollTop; - isUserScrollingUp = currentScrollTop < lastScrollTop.current; - lastScrollTop.current = currentScrollTop; - - // Reset the scrolling flag after user stops scrolling - scrollTimeout.current = setTimeout(() => { - //setIsUserScrollingUp(false); - }, 500); - }; - - // Add scroll event listener - container.addEventListener('scroll', handleScroll); - - // Run initial check - checkAndScrollToBottom(); - - // Cleanup - return () => { - if (resizeObserver) { - resizeObserver.disconnect(); - } - if (container) { - container.removeEventListener('scroll', handleScroll); - } - if (scrollTimeout.current) { - clearTimeout(scrollTimeout.current); - } - }; - }, [smooth, threshold]); // Re-run when dependencies change or scrolling state changes - - return containerRef as RefObject; -}; - -interface ScrollableProps { - children?: React.ReactNode; - sx?: SxProps, - autoscroll?: boolean, -} - -const Scrollable = (props: ScrollableProps) => { - const { sx, children, autoscroll } = props; - const scrollRef = useAutoScrollToBottom(); - return {children}; -}; - -export { - useAutoScrollToBottom, - Scrollable, -}; - diff --git a/frontend/src/BackstoryTextField.tsx b/frontend/src/BackstoryTextField.tsx new file mode 100644 index 0000000..17752ea --- /dev/null +++ b/frontend/src/BackstoryTextField.tsx @@ -0,0 +1,124 @@ +import React, { useRef, useEffect, ChangeEvent, KeyboardEvent } from 'react'; +import { useTheme } from '@mui/material/styles'; + +interface BackstoryTextFieldProps { + value: string; + disabled?: boolean; + multiline?: boolean; + placeholder?: string; + onChange?: (e: ChangeEvent) => void; + onKeyDown?: (e: KeyboardEvent) => void; +} + +const BackstoryTextField: React.FC = ({ + value, + disabled = false, + multiline = false, + placeholder, + onChange, + onKeyDown, +}) => { + const theme = useTheme(); + const textareaRef = useRef(null); + const shadowRef = useRef(null); + + useEffect(() => { + if (multiline && textareaRef.current && shadowRef.current) { + const shadow = shadowRef.current; + const textarea = textareaRef.current; + + shadow.value = value || placeholder || ''; + + window.setTimeout(() => { + const computed = getComputedStyle(textarea); + const paddingTop = parseFloat(computed.paddingTop || '0'); + const paddingBottom = parseFloat(computed.paddingBottom || '0'); + + const totalPadding = paddingTop + paddingBottom; + const newHeight = shadow.scrollHeight + totalPadding; + + textarea.style.height = `${newHeight}px`; + }, 100); + } + }, [value, multiline, placeholder]); + + const handleChange = (e: ChangeEvent) => { + if (onChange) { + onChange(e); + } + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (!multiline && e.key === 'Enter') { + e.preventDefault(); + } + if (onKeyDown) { + onKeyDown(e); + } + }; + + const sharedStyle = { + width: '100%', + padding: '14px', + resize: 'none' as const, + overflow: 'hidden' as const, + boxSizing: 'border-box' as const, + lineHeight: '1.5', + borderRadius: '0.25rem', + fontSize: '16px', + opacity: disabled ? "0.38" : "1", + color: disabled ? 'rgba(0, 0, 0, 0.38)' : 'rgb(46, 46, 46)', + backgroundColor: 'rgba(0, 0, 0, 0)', + fontFamily: theme.typography.fontFamily, + }; + + if (!multiline) { + return ( + + ); + } + + return ( + <> +