diff --git a/package-lock.json b/package-lock.json index 2ec8776e..6aa6318e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,6 +62,7 @@ "prettier": "3.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-dropzone": "^14.2.10", "react-markdown": "^9.0.1", "react-syntax-highlighter": "^15.5.0", "redux-persist": "^6.0.0", @@ -10422,6 +10423,15 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, + "node_modules/attr-accept": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.4.tgz", + "integrity": "sha512-2pA6xFIbdTUDCAwjN8nQwI+842VwzbDUXO2IYlpPXQIORgKnavorcr4Ce3rwh+zsNg9zK7QPsdvDj3Lum4WX4w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -13175,6 +13185,18 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-selector": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", + "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", + "dev": true, + "dependencies": { + "tslib": "^2.4.0" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/file-system-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/file-system-cache/-/file-system-cache-2.3.0.tgz", @@ -19477,6 +19499,23 @@ "react": "^18.2.0" } }, + "node_modules/react-dropzone": { + "version": "14.2.10", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.10.tgz", + "integrity": "sha512-Y98LOCYxGO2jOFWREeKJlL7gbrHcOlTBp+9DCM1dh9XQ8+P/8ThhZT7kFb05C+bPcTXq/rixpU+5+LzwYrFLUw==", + "dev": true, + "dependencies": { + "attr-accept": "^2.2.2", + "file-selector": "^0.6.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8 || 18.0.0" + } + }, "node_modules/react-element-to-jsx-string": { "version": "15.0.0", "resolved": "https://registry.npmjs.org/react-element-to-jsx-string/-/react-element-to-jsx-string-15.0.0.tgz", diff --git a/package.json b/package.json index dcf8ace7..f7906163 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "prettier": "3.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-dropzone": "^14.2.10", "react-markdown": "^9.0.1", "react-syntax-highlighter": "^15.5.0", "redux-persist": "^6.0.0", diff --git a/src/app/middleware.ts b/src/app/middleware.ts index 8622f42f..c12a4561 100644 --- a/src/app/middleware.ts +++ b/src/app/middleware.ts @@ -5,8 +5,9 @@ import { isRejected, } from "@reduxjs/toolkit"; import { - chatAskQuestionThunk, + doneStreaming, newChatAction, + chatAskQuestionThunk, restoreChat, } from "../features/Chat/Thread"; import { statisticsApi } from "../services/refact/statistics"; @@ -18,6 +19,7 @@ import { diffApi } from "../services/refact/diffs"; import { pingApi } from "../services/refact/ping"; import { clearError, setError } from "../features/Errors/errorsSlice"; import { updateConfig } from "../features/Config/configSlice"; +import { resetAttachedImagesSlice } from "../features/AttachedImages"; export const listenerMiddleware = createListenerMiddleware(); const startListening = listenerMiddleware.startListening.withTypes< @@ -34,13 +36,14 @@ startListening({ ), effect: (_action, listenerApi) => { [ + pingApi.util.resetApiState(), statisticsApi.util.resetApiState(), capsApi.util.resetApiState(), promptsApi.util.resetApiState(), toolsApi.util.resetApiState(), commandsApi.util.resetApiState(), diffApi.util.resetApiState(), - pingApi.util.resetApiState(), + resetAttachedImagesSlice(), ].forEach((api) => listenerApi.dispatch(api)); listenerApi.dispatch(clearError()); @@ -104,3 +107,13 @@ startListening({ } }, }); + +startListening({ + actionCreator: doneStreaming, + effect: (action, listenerApi) => { + const state = listenerApi.getState(); + if (action.payload.id === state.chat.thread.id) { + listenerApi.dispatch(resetAttachedImagesSlice()); + } + }, +}); diff --git a/src/app/store.ts b/src/app/store.ts index cc7fa97f..bb4e85f2 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -38,6 +38,7 @@ import { pagesSlice } from "../features/Pages/pagesSlice"; import mergeInitialState from "redux-persist/lib/stateReconciler/autoMergeLevel2"; import { listenerMiddleware } from "./middleware"; import { informationSlice } from "../features/Errors/informationSlice"; +import { attachedImagesSlice } from "../features/AttachedImages"; const tipOfTheDayPersistConfig = { key: "totd", @@ -75,6 +76,7 @@ const rootReducer = combineSlices( errorSlice, informationSlice, pagesSlice, + attachedImagesSlice, ); const rootPersistConfig = { diff --git a/src/components/Chat/Chat.tsx b/src/components/Chat/Chat.tsx index 52fb3f8d..a5a7213b 100644 --- a/src/components/Chat/Chat.tsx +++ b/src/components/Chat/Chat.tsx @@ -24,6 +24,7 @@ import { } from "../../features/Chat/Thread"; import { ThreadHistoryButton } from "../Buttons"; import { push } from "../../features/Pages/pagesSlice"; +import { DropzoneProvider } from "../Dropzone"; import { SystemPrompts } from "../../services/refact"; export type ChatProps = { @@ -113,72 +114,73 @@ export const Chat: React.FC = ({ }, [isWaiting, isStreaming, focusTextarea]); return ( - - - {!isStreaming && preventSend && unCalledTools && ( - - - - Chat was interrupted with uncalled tools calls. - - - - - )} - - - - {/* Two flexboxes are left for the future UI element on the right side */} - {messages.length > 0 && ( - - - model: {chatModel || caps.default_cap} •{" "} - setIsDebugChatHistoryVisible((prev) => !prev)} - > - mode: {chatToolUse}{" "} - - - {messages.length !== 0 && - !isStreaming && - isDebugChatHistoryVisible && ( - - )} - + + + + {!isStreaming && preventSend && unCalledTools && ( + + + + Chat was interrupted with uncalled tools calls. + + + + )} + + + {/* Two flexboxes are left for the future UI element on the right side */} + {messages.length > 0 && ( + + + model: {chatModel || caps.default_cap} •{" "} + setIsDebugChatHistoryVisible((prev) => !prev)} + > + mode: {chatToolUse}{" "} + + + {messages.length !== 0 && + !isStreaming && + isDebugChatHistoryVisible && ( + + )} + + )} + - + ); }; diff --git a/src/components/ChatContent/ChatContent.tsx b/src/components/ChatContent/ChatContent.tsx index b2d4aa8c..5346066a 100644 --- a/src/components/ChatContent/ChatContent.tsx +++ b/src/components/ChatContent/ChatContent.tsx @@ -4,6 +4,7 @@ import { isChatContextFileMessage, isDiffMessage, isToolMessage, + UserMessage, } from "../../services/refact"; import { UserInput } from "./UserInput"; import { ScrollArea } from "../ScrollArea"; @@ -110,7 +111,7 @@ const PlaceHolderText: React.FC = () => { }; export type ChatContentProps = { - onRetry: (index: number, question: string) => void; + onRetry: (index: number, question: UserMessage["content"]) => void; }; export const ChatContent = React.forwardRef( @@ -131,7 +132,10 @@ export const ChatContent = React.forwardRef( isStreaming, }); - const onRetryWrapper = (index: number, question: string) => { + const onRetryWrapper = ( + index: number, + question: UserMessage["content"], + ) => { props.onRetry(index, question); handleScrollButtonClick(); }; @@ -165,7 +169,7 @@ ChatContent.displayName = "ChatContent"; function renderMessages( messages: ChatMessages, - onRetry: (index: number, question: string) => void, + onRetry: (index: number, question: UserMessage["content"]) => void, memo: React.ReactNode[] = [], index = 0, ) { @@ -197,6 +201,7 @@ function renderMessages( if (head.role === "user") { const key = "user-input-" + index; + const nextMemo = [ ...memo, diff --git a/src/components/ChatContent/UserInput.tsx b/src/components/ChatContent/UserInput.tsx index b76d0dd4..8067f21a 100644 --- a/src/components/ChatContent/UserInput.tsx +++ b/src/components/ChatContent/UserInput.tsx @@ -1,50 +1,28 @@ import React, { useCallback, useState } from "react"; -import { Text, Container, Button, Flex, IconButton } from "@radix-ui/themes"; +import { + Text, + Container, + Button, + Flex, + IconButton, + Avatar, +} from "@radix-ui/themes"; import { Markdown } from "../Markdown"; import { RetryForm } from "../ChatForm"; import styles from "./ChatContent.module.css"; -import { Pencil2Icon } from "@radix-ui/react-icons"; - -function processLines( - lines: string[], - processedLinesMemo: JSX.Element[] = [], -): JSX.Element[] { - if (lines.length === 0) return processedLinesMemo; - - const [head, ...tail] = lines; - const nextBackTicksIndex = tail.findIndex((l) => l.startsWith("```")); - const key = `line-${processedLinesMemo.length + 1}`; - - if (!head.startsWith("```") || nextBackTicksIndex === -1) { - const processedLines = processedLinesMemo.concat( - - {head} - , - ); - return processLines(tail, processedLines); - } - - const endIndex = nextBackTicksIndex + 1; - - const code = [head].concat(tail.slice(0, endIndex)).join("\n"); - const processedLines = processedLinesMemo.concat( - {code}, - ); - - const next = tail.slice(endIndex); - return processLines(next, processedLines); -} +import { Pencil2Icon, ImageIcon } from "@radix-ui/react-icons"; +import { + ProcessedUserMessageContentWithImages, + UserMessageContentWithImage, + type UserMessage, +} from "../../services/refact"; +import { takeWhile } from "../../utils"; export type UserInputProps = { - children: string; + children: UserMessage["content"]; messageIndex: number; - onRetry: (index: number, question: string) => void; + // maybe add images argument ? + onRetry: (index: number, question: UserMessage["content"]) => void; // disableRetry?: boolean; }; @@ -56,9 +34,10 @@ export const UserInput: React.FC = ({ // const { retryFromIndex } = useSendChatRequest(); const [showTextArea, setShowTextArea] = useState(false); const [isEditButtonVisible, setIsEditButtonVisible] = useState(false); - const ref = React.useRef(null); + // const ref = React.useRef(null); + const handleSubmit = useCallback( - (value: string) => { + (value: UserMessage["content"]) => { onRetry(messageIndex, value); setShowTextArea(false); }, @@ -75,14 +54,18 @@ export const UserInput: React.FC = ({ [isEditButtonVisible], ); - const lines = children.split("\n"); - const elements = processLines(lines); + // const lines = children.split("\n"); // won't work if it's an array + const elements = process(children); + const isString = typeof children === "string"; + const linesLength = isString ? children.split("\n").length : Infinity; return ( {showTextArea ? ( handleShowTextArea(false)} /> @@ -90,17 +73,20 @@ export const UserInput: React.FC = ({ setIsEditButtonVisible(true)} onMouseLeave={() => setIsEditButtonVisible(false)} >