diff --git a/src/app/middleware.ts b/src/app/middleware.ts index 08b86d6e..8f855d26 100644 --- a/src/app/middleware.ts +++ b/src/app/middleware.ts @@ -20,6 +20,7 @@ import { pingApi } from "../services/refact/ping"; import { clearError, setError } from "../features/Errors/errorsSlice"; import { updateConfig } from "../features/Config/configSlice"; import { resetAttachedImagesSlice } from "../features/AttachedImages"; +import { nextTip } from "../features/TipOfTheDay"; export const listenerMiddleware = createListenerMiddleware(); const startListening = listenerMiddleware.startListening.withTypes< @@ -126,3 +127,25 @@ startListening({ } }, }); + +startListening({ + matcher: isAnyOf(restoreChat, newChatAction, updateConfig), + effect: (action, listenerApi) => { + const state = listenerApi.getState(); + const isUpdate = updateConfig.match(action); + + const host = + isUpdate && action.payload.host ? action.payload.host : state.config.host; + + const completeManual = isUpdate + ? action.payload.keyBindings?.completeManual + : state.config.keyBindings?.completeManual; + + listenerApi.dispatch( + nextTip({ + host, + completeManual, + }), + ); + }, +}); diff --git a/src/app/store.ts b/src/app/store.ts index 8f21c1b9..b6a6c055 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -23,7 +23,7 @@ import { import { smallCloudApi } from "../services/smallcloud"; import { reducer as fimReducer } from "../features/FIM/reducer"; import { tourReducer } from "../features/Tour"; -import { tipOfTheDayReducer } from "../features/TipOfTheDay"; +import { tipOfTheDaySlice } from "../features/TipOfTheDay"; import { reducer as configReducer } from "../features/Config/configSlice"; import { activeFileReducer } from "../features/Chat/activeFile"; import { selectedSnippetReducer } from "../features/Chat/selectedSnippet"; @@ -48,8 +48,8 @@ const tipOfTheDayPersistConfig = { }; const persistedTipOfTheDayReducer = persistReducer< - ReturnType ->(tipOfTheDayPersistConfig, tipOfTheDayReducer); + ReturnType +>(tipOfTheDayPersistConfig, tipOfTheDaySlice.reducer); // https://redux-toolkit.js.org/api/combineSlices // `combineSlices` automatically combines the reducers using @@ -58,7 +58,8 @@ const rootReducer = combineSlices( { fim: fimReducer, tour: tourReducer, - tipOfTheDay: persistedTipOfTheDayReducer, + // tipOfTheDay: persistedTipOfTheDayReducer, + [tipOfTheDaySlice.reducerPath]: persistedTipOfTheDayReducer, config: configReducer, active_file: activeFileReducer, selected_snippet: selectedSnippetReducer, @@ -104,9 +105,9 @@ export function setUpStore(preloadedState?: Partial) { const store = configureStore({ reducer: persistedReducer, preloadedState: initialState, - devTools: { - maxAge: 50, - }, + // devTools: { + // maxAge: 50, + // }, middleware: (getDefaultMiddleware) => { const production = import.meta.env.MODE === "production"; const middleware = production diff --git a/src/components/Chat/Chat.tsx b/src/components/Chat/Chat.tsx index 70108c53..27a46ef8 100644 --- a/src/components/Chat/Chat.tsx +++ b/src/components/Chat/Chat.tsx @@ -7,6 +7,7 @@ import { useAppDispatch, useSendChatRequest, useGetPromptsQuery, + useGetCapsQuery, } from "../../hooks"; import type { Config } from "../../features/Config/configSlice"; import { @@ -34,16 +35,31 @@ export type ChatProps = { style?: React.CSSProperties; unCalledTools: boolean; // TODO: update this - caps: ChatFormProps["caps"]; - maybeSendToSidebar: ChatFormProps["onClose"]; + // caps: ChatFormProps["caps"]; + // maybeSendToSidebar: ChatFormProps["onClose"]; }; export const Chat: React.FC = ({ style, unCalledTools, - caps, - maybeSendToSidebar, + // caps, // changes when clicking new chat :/ + // maybeSendToSidebar, }) => { + useEffect(() => { + console.log("style changed"); + }, [style]); + useEffect(() => { + console.log("uncalled tools change"); + }, [unCalledTools]); + + // useEffect(() => { + // console.log("caps changed"); + // }, [caps]); + + // useEffect(() => { + // console.log("side fn changed"); + // }, [maybeSendToSidebar]); + const [isViewingRawJSON, setIsViewingRawJSON] = useState(false); const chatContentRef = useRef(null); const isStreaming = useAppSelector(selectIsStreaming); @@ -64,12 +80,15 @@ export const Chat: React.FC = ({ const [isDebugChatHistoryVisible, setIsDebugChatHistoryVisible] = useState(false); + // TODO: can push this down, + const capsRequest = useGetCapsQuery(); const onSetChatModel = useCallback( (value: string) => { - const model = caps.default_cap === value ? "" : value; + const defaultModel = capsRequest.data?.code_chat_default_model ?? ""; + const model = defaultModel === value ? "" : value; dispatch(setChatModel(model)); }, - [caps.default_cap, dispatch], + [capsRequest.data?.code_chat_default_model, dispatch], ); const preventSend = useAppSelector(selectPreventSend); const onEnableSend = () => dispatch(enableSend({ id: chatId })); @@ -113,6 +132,16 @@ export const Chat: React.FC = ({ focusTextarea(); } }, [isWaiting, isStreaming, focusTextarea]); + console.log({ chatId }); + + // useEffect(() => { + // console.log("retry changed"); + // }, [retryFromIndex]); + + useEffect(() => { + console.log("chat mounted"); + return () => console.log("chat unmount"); + }, []); return ( @@ -128,7 +157,7 @@ export const Chat: React.FC = ({ {!isStreaming && preventSend && unCalledTools && ( @@ -148,9 +177,15 @@ export const Chat: React.FC = ({ onSubmit={handleSummit} model={chatModel} onSetChatModel={onSetChatModel} - caps={caps} + // caps={caps} + caps={{ + error: capsRequest.error ? "error fetching caps" : null, + fetching: capsRequest.isFetching, + default_cap: capsRequest.data?.code_chat_default_model ?? "", + available_caps: capsRequest.data?.code_chat_models ?? {}, + }} onStopStreaming={abort} - onClose={maybeSendToSidebar} + // onClose={maybeSendToSidebar} onTextAreaHeightChange={onTextAreaHeightChange} prompts={promptsRequest.data ?? {}} onSetSystemPrompt={onSetSelectedSystemPrompt} @@ -162,7 +197,11 @@ export const Chat: React.FC = ({ {messages.length > 0 && ( - model: {chatModel || caps.default_cap} •{" "} + + model:{" "} + {chatModel || capsRequest.data?.code_chat_default_model}{" "} + {" "} + •{" "} setIsDebugChatHistoryVisible((prev) => !prev)} diff --git a/src/components/ChatContent/ChatContent.tsx b/src/components/ChatContent/ChatContent.tsx index 5346066a..bd77c76c 100644 --- a/src/components/ChatContent/ChatContent.tsx +++ b/src/components/ChatContent/ChatContent.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect } from "react"; +import React, { useCallback } from "react"; import { ChatMessages, isChatContextFileMessage, @@ -15,10 +15,8 @@ import { ContextFiles } from "./ContextFiles"; import { AssistantInput } from "./AssistantInput"; import { useAutoScroll } from "./useAutoScroll"; import { PlainText } from "./PlainText"; -import { useConfig, useEventsBusForIDE } from "../../hooks"; -import { useAppSelector, useAppDispatch } from "../../hooks"; -import { RootState } from "../../app/store"; -import { next } from "../../features/TipOfTheDay"; +import { useConfig, useEventsBusForIDE, useSendChatRequest } from "../../hooks"; +import { useAppSelector } from "../../hooks"; import { selectIsStreaming, selectIsWaiting, @@ -27,25 +25,18 @@ import { import { takeWhile } from "../../utils"; import { GroupedDiffs } from "./DiffContent"; import { ScrollToBottomButton } from "./ScrollToBottomButton"; +import { currentTipOfTheDay } from "../../features/TipOfTheDay"; -export const TipOfTheDay: React.FC = () => { - const dispatch = useAppDispatch(); - const config = useConfig(); - const state = useAppSelector((state: RootState) => state.tipOfTheDay); - - // TODO: find out what this is about. - useEffect(() => { - dispatch(next(config)); - }, [dispatch, config]); +const TipOfTheDay: React.FC = () => { + const tip = useAppSelector(currentTipOfTheDay); return ( - 💡 Tip of the day: {state.tip} + 💡 Tip of the day: {tip} ); }; -// TODO: turn this into a component const PlaceHolderText: React.FC = () => { const config = useConfig(); const hasVecDB = config.features?.vecdb ?? false; @@ -110,60 +101,55 @@ const PlaceHolderText: React.FC = () => { ); }; -export type ChatContentProps = { - onRetry: (index: number, question: UserMessage["content"]) => void; -}; - -export const ChatContent = React.forwardRef( - (props, ref) => { - const messages = useAppSelector(selectMessages); - const isStreaming = useAppSelector(selectIsStreaming); - const isWaiting = useAppSelector(selectIsWaiting); - - const { - innerRef, - handleScroll, - handleWheel, - handleScrollButtonClick, - isScrolledTillBottom, - } = useAutoScroll({ - ref, - messages, - isStreaming, - }); - - const onRetryWrapper = ( - index: number, - question: UserMessage["content"], - ) => { - props.onRetry(index, question); +export const ChatContent = React.forwardRef((_props, ref) => { + const messages = useAppSelector(selectMessages); + const isStreaming = useAppSelector(selectIsStreaming); + const isWaiting = useAppSelector(selectIsWaiting); + const { retryFromIndex } = useSendChatRequest(); + + const { + innerRef, + handleScroll, + handleWheel, + handleScrollButtonClick, + isScrolledTillBottom, + } = useAutoScroll({ + ref, + messages, + isStreaming, + }); + + const onRetryWrapper = useCallback( + (index: number, question: UserMessage["content"]) => { + retryFromIndex(index, question); handleScrollButtonClick(); - }; + }, + [handleScrollButtonClick, retryFromIndex], + ); - return ( - - - {messages.length === 0 && } - {renderMessages(messages, onRetryWrapper)} - {isWaiting && ( - - - - )} -
- - {!isScrolledTillBottom && ( - + return ( + + + {messages.length === 0 && } + {renderMessages(messages, onRetryWrapper)} + {isWaiting && ( + + + )} - - ); - }, -); +
+ + {!isScrolledTillBottom && ( + + )} + + ); +}); ChatContent.displayName = "ChatContent"; diff --git a/src/components/ChatForm/ChatForm.tsx b/src/components/ChatForm/ChatForm.tsx index 1bc9d93f..00898e16 100644 --- a/src/components/ChatForm/ChatForm.tsx +++ b/src/components/ChatForm/ChatForm.tsx @@ -297,6 +297,7 @@ export const ChatForm: React.FC = ({ checkboxes={checkboxes} showControls={showControls} onCheckedChange={onToggleCheckbox} + // can be moved down ? selectProps={{ value: model || caps.default_cap, onChange: onSetChatModel, diff --git a/src/features/App.tsx b/src/features/App.tsx index e9163c9a..bb6779b0 100644 --- a/src/features/App.tsx +++ b/src/features/App.tsx @@ -132,9 +132,9 @@ export const InnerApp: React.FC = ({ style }: AppProps) => { dispatch(push({ name: "history" })); }; - const goBack = () => { + const goBack = useCallback(() => { dispatch(pop()); - }; + }, [dispatch]); const page = pages[pages.length - 1]; @@ -191,6 +191,7 @@ export const InnerApp: React.FC = ({ style }: AppProps) => { )} {page.name === "chat" && ( void; }; export const Chat: React.FC = ({ - style, + // style, backFromChat, host, tabbed, }) => { - const capsRequest = useGetCapsQuery(); + // const capsRequest = useGetCapsQuery(); + + useEffect(() => { + console.log("Chat Feature unmounted"); + return () => console.log("Chat Feature unmounted"); + }, []); const messages = useAppSelector(selectMessages); + useAutoSubmit(); - const sendToSideBar = () => { - // TODO: - }; + // const sendToSideBar = () => { + // // TODO: + // }; - const maybeSendToSideBar = - host === "vscode" && tabbed ? sendToSideBar : undefined; + // const maybeSendToSideBar = + // host === "vscode" && tabbed ? sendToSideBar : undefined; - // can be a selector + // TODO: can be a selector const unCalledTools = React.useMemo(() => { if (messages.length === 0) return false; const last = messages[messages.length - 1]; @@ -41,19 +47,22 @@ export const Chat: React.FC = ({ return ( ({})} /> diff --git a/src/features/Config/configSlice.ts b/src/features/Config/configSlice.ts index fa791639..89e58165 100644 --- a/src/features/Config/configSlice.ts +++ b/src/features/Config/configSlice.ts @@ -86,3 +86,6 @@ export const selectAddressURL = (state: RootState) => state.config.addressURL; export const selectHost = (state: RootState) => state.config.host; export const selectSubmitOption = (state: RootState) => state.config.shiftEnterToSubmit ?? false; + +export const selectCompleteManual = (state: RootState) => + state.config.keyBindings?.completeManual; diff --git a/src/features/TipOfTheDay.tsx b/src/features/TipOfTheDay.tsx index ef0f21a3..558cb100 100644 --- a/src/features/TipOfTheDay.tsx +++ b/src/features/TipOfTheDay.tsx @@ -1,4 +1,4 @@ -import { createAction, createReducer } from "@reduxjs/toolkit"; +import { createSlice } from "@reduxjs/toolkit"; import type { Config } from "../features/Config/configSlice"; type TipHost = "all" | "vscode"; @@ -45,52 +45,59 @@ export const tips: [TipHost, string][] = [ ]; export type TipOfTheDayState = { - next: number; + current: number; tip: string; }; const initialState: TipOfTheDayState = { - next: 0, - tip: "", + current: 0, + tip: "", //tips[0][1], // make sure can be all }; -export const next = createAction("tipOfTheDay/next"); - -export const tipOfTheDayReducer = createReducer( +export const tipOfTheDaySlice = createSlice({ + name: "tipOfTheDay", initialState, - (builder) => { - builder.addCase(next, (state, action) => { - const keyBindings = action.payload.keyBindings; - const host = action.payload.host; + reducers: (create) => ({ + nextTip: create.reducer<{ host: Config["host"]; completeManual?: string }>( + (state, action) => { + const { host, completeManual } = action.payload; + console.log("Next Tip called"); + console.log({ host, completeManual }); + // const keyBindings = action.payload.keyBindings; + // const host = action.payload.host; - let tip: string | undefined = undefined; - let next = state.next; + let tip: string | undefined = undefined; + let nextIndex = (state.current + 1) % tips.length; - while (tip === undefined) { - const [tipHost, curTip] = tips[next]; - next = (next + 1) % tips.length; + while (tip === undefined) { + const [tipHost, curTip] = tips[nextIndex]; + nextIndex = (nextIndex + 1) % tips.length; - if (!matchesHost(tipHost, host)) { - continue; - } + if (!matchesHost(tipHost, host)) { + continue; + } - if (keyBindings?.completeManual !== undefined) { - tip = curTip.replace( - "[MANUAL_COMPLETION]", - keyBindings.completeManual, - ); - } else { - tip = curTip.replace( - "[MANUAL_COMPLETION]", - "the key binding for manual completion", - ); + if (completeManual !== undefined) { + tip = curTip.replace("[MANUAL_COMPLETION]", completeManual); + } else { + tip = curTip.replace( + "[MANUAL_COMPLETION]", + "the key binding for manual completion", + ); + } } - } - - return { - next, - tip, - }; - }); + console.log({ nextIndex, tip }); + return { + current: nextIndex, + tip, + }; + }, + ), + }), + selectors: { + currentTipOfTheDay: (state) => state.tip, }, -); +}); + +export const { nextTip } = tipOfTheDaySlice.actions; +export const { currentTipOfTheDay } = tipOfTheDaySlice.selectors; diff --git a/src/hooks/useSendChatRequest.ts b/src/hooks/useSendChatRequest.ts index bc5a70f5..74725eef 100644 --- a/src/hooks/useSendChatRequest.ts +++ b/src/hooks/useSendChatRequest.ts @@ -42,18 +42,18 @@ let recallCounter = 0; export const useSendChatRequest = () => { const dispatch = useAppDispatch(); - const hasError = useAppSelector(selectChatError); + // const hasError = useAppSelector(selectChatError); const abortControllers = useAbortControllers(); const [triggerGetTools] = useGetToolsLazyQuery(); const [triggerCheckForConfirmation] = useCheckForConfirmationMutation(); const chatId = useAppSelector(selectChatId); - const streaming = useAppSelector(selectIsStreaming); - const chatError = useAppSelector(selectChatError); + // const streaming = useAppSelector(selectIsStreaming); + // const chatError = useAppSelector(selectChatError); - const errored: boolean = !!hasError || !!chatError; - const preventSend = useAppSelector(selectPreventSend); + // const errored: boolean = !!hasError || !!chatError; + // const preventSend = useAppSelector(selectPreventSend); const isWaiting = useAppSelector(selectIsWaiting); const currentMessages = useAppSelector(selectMessages); @@ -178,36 +178,36 @@ export const useSendChatRequest = () => { } }, [sendImmediately, sendMessages, messagesWithSystemPrompt]); - // TODO: Automatically calls tool calls. This means that this hook can only be used once :/ - // TODO: Think of better way to manage useEffect calls, not with outer counter - useEffect(() => { - if (!streaming && currentMessages.length > 0 && !errored && !preventSend) { - const lastMessage = currentMessages.slice(-1)[0]; - if ( - isAssistantMessage(lastMessage) && - lastMessage.tool_calls && - lastMessage.tool_calls.length > 0 - ) { - if (!areToolsConfirmed) { - abort(); - if (recallCounter < 1) { - recallCounter++; - return; - } - } - void sendMessages(currentMessages); - recallCounter = 0; - } - } - }, [ - errored, - currentMessages, - preventSend, - sendMessages, - abort, - streaming, - areToolsConfirmed, - ]); + // // TODO: Automatically calls tool calls. This means that this hook can only be used once :/ + // // TODO: Think of better way to manage useEffect calls, not with outer counter + // useEffect(() => { + // if (!streaming && currentMessages.length > 0 && !errored && !preventSend) { + // const lastMessage = currentMessages.slice(-1)[0]; + // if ( + // isAssistantMessage(lastMessage) && + // lastMessage.tool_calls && + // lastMessage.tool_calls.length > 0 + // ) { + // if (!areToolsConfirmed) { + // abort(); + // if (recallCounter < 1) { + // recallCounter++; + // return; + // } + // } + // void sendMessages(currentMessages); + // recallCounter = 0; + // } + // } + // }, [ + // errored, + // currentMessages, + // preventSend, + // sendMessages, + // abort, + // streaming, + // areToolsConfirmed, + // ]); const retry = useCallback( (messages: ChatMessages) => { @@ -240,5 +240,56 @@ export const useSendChatRequest = () => { retry, retryFromIndex, confirmToolUsage, + sendMessages, }; }; + +// WARNING: only use this once in the component tree +export const useAutoSubmit = () => { + const chatId = useAppSelector(selectChatId); + const streaming = useAppSelector(selectIsStreaming); + const chatError = useAppSelector(selectChatError); + const hasError = useAppSelector(selectChatError); + const currentMessages = useAppSelector(selectMessages); + + const errored: boolean = !!hasError || !!chatError; + const preventSend = useAppSelector(selectPreventSend); + const areToolsConfirmed = useAppSelector(getToolsConfirmationStatus); + const abortControllers = useAbortControllers(); + // TODO: Think of better way to manage useEffect calls, not with outer counter + + const abort = useCallback(() => { + abortControllers.abort(chatId); + }, [abortControllers, chatId]); + + const { sendMessages } = useSendChatRequest(); + + useEffect(() => { + if (!streaming && currentMessages.length > 0 && !errored && !preventSend) { + const lastMessage = currentMessages.slice(-1)[0]; + if ( + isAssistantMessage(lastMessage) && + lastMessage.tool_calls && + lastMessage.tool_calls.length > 0 + ) { + if (!areToolsConfirmed) { + abort(); + if (recallCounter < 1) { + recallCounter++; + return; + } + } + void sendMessages(currentMessages); + recallCounter = 0; + } + } + }, [ + errored, + currentMessages, + preventSend, + sendMessages, + abort, + streaming, + areToolsConfirmed, + ]); +};