diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx deleted file mode 100644 index 93a04d2..0000000 --- a/src/components/ChatInterface.jsx +++ /dev/null @@ -1,5696 +0,0 @@ -/* - * ChatInterface.jsx - Chat Component with Session Protection Integration - * - * SESSION PROTECTION INTEGRATION: - * =============================== - * - * This component integrates with the Session Protection System to prevent project updates - * from interrupting active conversations: - * - * Key Integration Points: - * 1. handleSubmit() - Marks session as active when user sends message (including temp ID for new sessions) - * 2. session-created handler - Replaces temporary session ID with real WebSocket session ID - * 3. claude-complete handler - Marks session as inactive when conversation finishes - * 4. session-aborted handler - Marks session as inactive when conversation is aborted - * - * This ensures uninterrupted chat experience by coordinating with App.jsx to pause sidebar updates. - */ - -import React, { useState, useEffect, useRef, useMemo, useCallback, useLayoutEffect, memo } from 'react'; -import ReactMarkdown from 'react-markdown'; -import remarkGfm from 'remark-gfm'; -import remarkMath from 'remark-math'; -import rehypeKatex from 'rehype-katex'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; -import { useDropzone } from 'react-dropzone'; -import TodoList from './TodoList'; -import ClaudeLogo from './ClaudeLogo.jsx'; -import CursorLogo from './CursorLogo.jsx'; -import CodexLogo from './CodexLogo.jsx'; -import NextTaskBanner from './NextTaskBanner.jsx'; -import QuickSettingsPanel from './QuickSettingsPanel'; - -import { useTasksSettings } from '../contexts/TasksSettingsContext'; -import { useTranslation } from 'react-i18next'; - -import ClaudeStatus from './ClaudeStatus'; -import TokenUsagePie from './TokenUsagePie'; -import { MicButton } from './MicButton.jsx'; -import { api, authenticatedFetch } from '../utils/api'; -import ThinkingModeSelector, { thinkingModes } from './ThinkingModeSelector.jsx'; -import Fuse from 'fuse.js'; -import CommandMenu from './CommandMenu'; -import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants'; - -import { safeJsonParse } from '../lib/utils.js'; - -// ! Move all utility functions to utils/chatUtils.ts - -// Helper function to decode HTML entities in text -function decodeHtmlEntities(text) { - if (!text) return text; - return text - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, "'") - .replace(/&/g, '&'); -} - -// Normalize markdown text where providers mistakenly wrap short inline code with single-line triple fences. -// Only convert fences that do NOT contain any newline to avoid touching real code blocks. -function normalizeInlineCodeFences(text) { - if (!text || typeof text !== 'string') return text; - try { - // ```code``` -> `code` - return text.replace(/```\s*([^\n\r]+?)\s*```/g, '`$1`'); - } catch { - return text; - } -} - -// Unescape \n, \t, \r while protecting LaTeX formulas ($...$ and $$...$$) from being corrupted -function unescapeWithMathProtection(text) { - if (!text || typeof text !== 'string') return text; - - const mathBlocks = []; - const PLACEHOLDER_PREFIX = '__MATH_BLOCK_'; - const PLACEHOLDER_SUFFIX = '__'; - - // Extract and protect math formulas - let processedText = text.replace(/\$\$([\s\S]*?)\$\$|\$([^\$\n]+?)\$/g, (match) => { - const index = mathBlocks.length; - mathBlocks.push(match); - return `${PLACEHOLDER_PREFIX}${index}${PLACEHOLDER_SUFFIX}`; - }); - - // Process escape sequences on non-math content - processedText = processedText.replace(/\\n/g, '\n') - .replace(/\\t/g, '\t') - .replace(/\\r/g, '\r'); - - // Restore math formulas - processedText = processedText.replace( - new RegExp(`${PLACEHOLDER_PREFIX}(\\d+)${PLACEHOLDER_SUFFIX}`, 'g'), - (match, index) => { - return mathBlocks[parseInt(index)]; - } - ); - - return processedText; -} - -function escapeRegExp(value) { - return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} - -// Small wrapper to keep markdown behavior consistent in one place -const Markdown = ({ children, className }) => { - const content = normalizeInlineCodeFences(String(children ?? '')); - const remarkPlugins = useMemo(() => [remarkGfm, remarkMath], []); - const rehypePlugins = useMemo(() => [rehypeKatex], []); - - return ( -
- {children}
-
- );
- }
-
- // Extract language from className (format: language-xxx)
- const match = /language-(\w+)/.exec(className || '');
- const language = match ? match[1] : 'text';
- const textToCopy = raw;
-
- const handleCopy = () => {
- const doSet = () => {
- setCopied(true);
- setTimeout(() => setCopied(false), 1500);
- };
- try {
- if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(textToCopy).then(doSet).catch(() => {
- // Fallback
- const ta = document.createElement('textarea');
- ta.value = textToCopy;
- ta.style.position = 'fixed';
- ta.style.opacity = '0';
- document.body.appendChild(ta);
- ta.select();
- try { document.execCommand('copy'); } catch {}
- document.body.removeChild(ta);
- doSet();
- });
- } else {
- const ta = document.createElement('textarea');
- ta.value = textToCopy;
- ta.style.position = 'fixed';
- ta.style.opacity = '0';
- document.body.appendChild(ta);
- ta.select();
- try { document.execCommand('copy'); } catch {}
- document.body.removeChild(ta);
- doSet();
- }
- } catch {}
- };
-
- // Code block with syntax highlighting
- return (
- - {children} -- ), - a: ({ href, children }) => ( - - {children} - - ), - p: ({ children }) =>
- {message.toolInput}
-
-
- {message.toolInput}
-
-
- {message.toolInput}
-
-
- {message.toolInput}
-
-
- {message.toolInput}
-
- {beforePrompt}
- - {questionLine} -
- - {/* Option buttons */} -- ✓ Claude selected option {selectedOption} -
-- In the CLI, you would select this option interactively using arrow keys or by typing the number. -
-- The file content is displayed in the diff view above -
-- {questionLine} -
- - {/* Option buttons */} -- ⏳ Waiting for your response in the CLI -
-- Please select an option in your terminal where Claude is running. -
-
-
- {formatted}
-
-
- Select a project to start chatting with Claude
-{t('session.loading.sessionMessages')}
-- {t('providerSelection.description')} -
- -- {provider === 'claude' - ? t('providerSelection.readyPrompt.claude', { model: claudeModel }) - : provider === 'cursor' - ? t('providerSelection.readyPrompt.cursor', { model: cursorModel }) - : provider === 'codex' - ? t('providerSelection.readyPrompt.codex', { model: codexModel }) - : t('providerSelection.readyPrompt.default') - } -
- - {/* Show NextTaskBanner when provider is selected and ready, only if TaskMaster is installed */} - {provider && tasksEnabled && isTaskMasterInstalled && ( -{t('session.continue.title')}
-- {t('session.continue.description')} -
- - {/* Show NextTaskBanner for existing sessions too, only if TaskMaster is installed */} - {tasksEnabled && isTaskMasterInstalled && ( -{t('session.loading.olderMessages')}
-
- {rawInput}
-
- Select a project to start chatting with Claude
+
+ {children}
+
+ );
+ }
+
+ const match = /language-(\w+)/.exec(className || '');
+ const language = match ? match[1] : 'text';
+ const textToCopy = raw;
+
+ const handleCopy = () => {
+ const doSet = () => {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1500);
+ };
+ try {
+ if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard.writeText(textToCopy).then(doSet).catch(() => {
+ const ta = document.createElement('textarea');
+ ta.value = textToCopy;
+ ta.style.position = 'fixed';
+ ta.style.opacity = '0';
+ document.body.appendChild(ta);
+ ta.select();
+ try {
+ document.execCommand('copy');
+ } catch {}
+ document.body.removeChild(ta);
+ doSet();
+ });
+ } else {
+ const ta = document.createElement('textarea');
+ ta.value = textToCopy;
+ ta.style.position = 'fixed';
+ ta.style.opacity = '0';
+ document.body.appendChild(ta);
+ ta.select();
+ try {
+ document.execCommand('copy');
+ } catch {}
+ document.body.removeChild(ta);
+ doSet();
+ }
+ } catch {}
+ };
+
+ return (
+ + {children} ++ ), + a: ({ href, children }: { href?: string; children?: React.ReactNode }) => ( + + {children} + + ), + p: ({ children }: { children?: React.ReactNode }) =>
+ {message.toolInput}
+
+
+ {message.toolInput}
+
+
+ {message.toolInput}
+
+
+ {message.toolInput}
+
+
+ {message.toolInput}
+
+ {beforePrompt}
+ + {questionLine} +
+ + {/* Option buttons */} ++ ✓ Claude selected option {selectedOption} +
++ In the CLI, you would select this option interactively using arrow keys or by typing the number. +
++ The file content is displayed in the diff view above +
++ {questionLine} +
+ + {/* Option buttons */} ++ ⏳ Waiting for your response in the CLI +
++ Please select an option in your terminal where Claude is running. +
+
+
+ {formatted}
+
+
+ {t('session.loading.sessionMessages')}
+{t('session.loading.olderMessages')}
+
+ {rawInput}
+
+ {t('providerSelection.description')}
+ ++ {provider === 'claude' + ? t('providerSelection.readyPrompt.claude', { model: claudeModel }) + : provider === 'cursor' + ? t('providerSelection.readyPrompt.cursor', { model: cursorModel }) + : provider === 'codex' + ? t('providerSelection.readyPrompt.codex', { model: codexModel }) + : t('providerSelection.readyPrompt.default')} +
+ + {provider && tasksEnabled && isTaskMasterInstalled && ( +{t('session.continue.title')}
+{t('session.continue.description')}
+ + {tasksEnabled && isTaskMasterInstalled && ( +