({
initialPrompt,
initialImages,
autoSendInitialPrompt,
})
| 607 | } |
| 608 | |
| 609 | export const DataLoadingChat: React.FC<DataLoadingChatProps> = ({ |
| 610 | initialPrompt, |
| 611 | initialImages, |
| 612 | autoSendInitialPrompt, |
| 613 | }) => { |
| 614 | const theme = useTheme(); |
| 615 | const { t } = useTranslation(); |
| 616 | const dispatch = useDispatch<AppDispatch>(); |
| 617 | |
| 618 | const chatMessages = useSelector((state: DataFormulatorState) => state.dataLoadingChatMessages); |
| 619 | const chatInProgress = useSelector((state: DataFormulatorState) => state.dataLoadingChatInProgress); |
| 620 | // External reset signal — bumped by `clearChatMessages` (manual reset |
| 621 | // button, new menu-level query, full session reset). When it changes |
| 622 | // we abort any in-flight stream, drop partial UI state, and re-seed |
| 623 | // from props if the parent provided a new prompt/images. Without |
| 624 | // this, an in-flight stream's eventual dispatches would leak into |
| 625 | // the freshly-cleared thread. |
| 626 | const chatResetCounter = useSelector((state: DataFormulatorState) => state.dataLoadingChatResetCounter ?? 0); |
| 627 | const existingTables = useSelector((state: DataFormulatorState) => state.tables); |
| 628 | const activeModel = useSelector(dfSelectors.getActiveModel); |
| 629 | const frontendRowLimit = useSelector((state: DataFormulatorState) => state.config?.frontendRowLimit ?? 2_000_000); |
| 630 | const existingNames = new Set(existingTables.map(tbl => tbl.id)); |
| 631 | |
| 632 | const [prompt, setPrompt] = useState(''); |
| 633 | const [userImages, setUserImages] = useState<string[]>([]); |
| 634 | const [userAttachments, setUserAttachments] = useState<string[]>([]); |
| 635 | const [streamingContent, setStreamingContent] = useState(''); |
| 636 | const [streamingToolSteps, setStreamingToolSteps] = useState<ToolStep[]>([]); |
| 637 | const [debugEvents, setDebugEvents] = useState<any[]>([]); |
| 638 | const [showDebugPanel] = useState(false); |
| 639 | const abortControllerRef = useRef<AbortController | null>(null); |
| 640 | // Monotonic session token. Bumped on every external reset; the |
| 641 | // currently-running `sendMessage` captures the value at the time |
| 642 | // it started and discards any state/dispatch updates if the token |
| 643 | // has moved on (i.e. the user reset / restarted the chat mid-stream). |
| 644 | const sessionRef = useRef(0); |
| 645 | const lastResetRef = useRef(chatResetCounter); |
| 646 | const messagesEndRef = useRef<HTMLDivElement>(null); |
| 647 | const inputRef = useRef<HTMLTextAreaElement>(null); |
| 648 | |
| 649 | // Auto-scroll to bottom |
| 650 | useEffect(() => { |
| 651 | messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); |
| 652 | }, [chatMessages, streamingContent]); |
| 653 | |
| 654 | // Auto-focus input |
| 655 | useEffect(() => { inputRef.current?.focus(); }, []); |
| 656 | |
| 657 | // ---- External initial prompt handling ------------------------------- |
| 658 | // Pre-fill the input (and optionally auto-send) when `initialPrompt` |
| 659 | // is provided. Used by external surfaces (e.g. landing-page quick chat |
| 660 | // box) to hand off text to the agent. Auto-send only fires for a |
| 661 | // fresh conversation — we never auto-resend on remount mid-chat. |
| 662 | const hasExistingMessages = chatMessages.length > 0; |
| 663 | const [pendingAutoSend, setPendingAutoSend] = useState(false); |
| 664 | useEffect(() => { |
| 665 | // Detect external reset: abort, invalidate in-flight session, |
| 666 | // and clear all local UI state before re-seeding. Including |
nothing calls this directly
no test coverage detected