From 2e7b626b004b57e0828a237857fe273425b0622e Mon Sep 17 00:00:00 2001 From: KevIsDev Date: Mon, 26 May 2025 16:05:51 +0100 Subject: [PATCH 1/5] feat: add discuss mode and quick actions - Implement discuss mode toggle and UI in chat box - Add quick action buttons for file, message, implement and link actions - Extend markdown parser to handle quick action elements - Update message components to support discuss mode and quick actions - Add discuss prompt for technical consulting responses - Refactor chat components to support new functionality The changes introduce a new discuss mode that allows users to switch between code implementation and technical discussion modes. Quick action buttons provide immediate interaction options like opening files, sending messages, or switching modes. --- app/components/chat/Artifact.tsx | 2 +- app/components/chat/AssistantMessage.tsx | 187 +++++++------- app/components/chat/BaseChat.tsx | 297 ++++------------------ app/components/chat/Chat.client.tsx | 5 + app/components/chat/ChatBox.tsx | 310 +++++++++++++++++++++++ app/components/chat/DicussMode.tsx | 17 ++ app/components/chat/Markdown.tsx | 166 ++++++++---- app/components/chat/Messages.client.tsx | 6 + app/lib/.server/llm/stream-text.ts | 37 +-- app/lib/common/prompts/discuss-prompt.ts | 235 +++++++++++++++++ app/lib/runtime/message-parser.ts | 51 ++++ app/routes/api.chat.ts | 5 +- app/utils/markdown.ts | 12 +- 13 files changed, 922 insertions(+), 408 deletions(-) create mode 100644 app/components/chat/ChatBox.tsx create mode 100644 app/components/chat/DicussMode.tsx create mode 100644 app/lib/common/prompts/discuss-prompt.ts diff --git a/app/components/chat/Artifact.tsx b/app/components/chat/Artifact.tsx index 063b0b5..cef5a39 100644 --- a/app/components/chat/Artifact.tsx +++ b/app/components/chat/Artifact.tsx @@ -184,7 +184,7 @@ const actionVariants = { visible: { opacity: 1, y: 0 }, }; -function openArtifactInWorkbench(filePath: any) { +export function openArtifactInWorkbench(filePath: any) { if (workbenchStore.currentView.get() !== 'code') { workbenchStore.currentView.set('code'); } diff --git a/app/components/chat/AssistantMessage.tsx b/app/components/chat/AssistantMessage.tsx index aa831a4..2d013b8 100644 --- a/app/components/chat/AssistantMessage.tsx +++ b/app/components/chat/AssistantMessage.tsx @@ -5,6 +5,7 @@ import Popover from '~/components/ui/Popover'; import { workbenchStore } from '~/lib/stores/workbench'; import { WORK_DIR } from '~/utils/constants'; import WithTooltip from '~/components/ui/Tooltip'; +import type { Message } from 'ai'; interface AssistantMessageProps { content: string; @@ -12,6 +13,9 @@ interface AssistantMessageProps { messageId?: string; onRewind?: (messageId: string) => void; onFork?: (messageId: string) => void; + append?: (message: Message) => void; + chatMode?: 'discuss' | 'build'; + setChatMode?: (mode: 'discuss' | 'build') => void; } function openArtifactInWorkbench(filePath: string) { @@ -38,104 +42,109 @@ function normalizedFilePath(path: string) { return normalizedPath; } -export const AssistantMessage = memo(({ content, annotations, messageId, onRewind, onFork }: AssistantMessageProps) => { - const filteredAnnotations = (annotations?.filter( - (annotation: JSONValue) => annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'), - ) || []) as { type: string; value: any } & { [key: string]: any }[]; +export const AssistantMessage = memo( + ({ content, annotations, messageId, onRewind, onFork, append, chatMode, setChatMode }: AssistantMessageProps) => { + const filteredAnnotations = (annotations?.filter( + (annotation: JSONValue) => + annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'), + ) || []) as { type: string; value: any } & { [key: string]: any }[]; - let chatSummary: string | undefined = undefined; + let chatSummary: string | undefined = undefined; - if (filteredAnnotations.find((annotation) => annotation.type === 'chatSummary')) { - chatSummary = filteredAnnotations.find((annotation) => annotation.type === 'chatSummary')?.summary; - } + if (filteredAnnotations.find((annotation) => annotation.type === 'chatSummary')) { + chatSummary = filteredAnnotations.find((annotation) => annotation.type === 'chatSummary')?.summary; + } - let codeContext: string[] | undefined = undefined; + let codeContext: string[] | undefined = undefined; - if (filteredAnnotations.find((annotation) => annotation.type === 'codeContext')) { - codeContext = filteredAnnotations.find((annotation) => annotation.type === 'codeContext')?.files; - } + if (filteredAnnotations.find((annotation) => annotation.type === 'codeContext')) { + codeContext = filteredAnnotations.find((annotation) => annotation.type === 'codeContext')?.files; + } - const usage: { - completionTokens: number; - promptTokens: number; - totalTokens: number; - } = filteredAnnotations.find((annotation) => annotation.type === 'usage')?.value; + const usage: { + completionTokens: number; + promptTokens: number; + totalTokens: number; + } = filteredAnnotations.find((annotation) => annotation.type === 'usage')?.value; - return ( -
- <> -
- {(codeContext || chatSummary) && ( - }> - {chatSummary && ( -
-
-

Summary

-
- {chatSummary} -
-
- {codeContext && ( -
-

Context

-
- {codeContext.map((x) => { - const normalized = normalizedFilePath(x); - return ( - - { - e.preventDefault(); - e.stopPropagation(); - openArtifactInWorkbench(normalized); - }} - > - {normalized} - - - ); - })} + return ( +
+ <> +
+ {(codeContext || chatSummary) && ( + }> + {chatSummary && ( +
+
+

Summary

+
+ {chatSummary}
+ {codeContext && ( +
+

Context

+
+ {codeContext.map((x) => { + const normalized = normalizedFilePath(x); + return ( + + { + e.preventDefault(); + e.stopPropagation(); + openArtifactInWorkbench(normalized); + }} + > + {normalized} + + + ); + })} +
+
+ )} +
+ )} +
+
+ )} +
+ {usage && ( +
+ Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens}) +
+ )} + {(onRewind || onFork) && messageId && ( +
+ {onRewind && ( + +
)} -
- - )} -
- {usage && ( -
- Tokens: {usage.totalTokens} (prompt: {usage.promptTokens}, completion: {usage.completionTokens}) -
- )} - {(onRewind || onFork) && messageId && ( -
- {onRewind && ( - -
- )} +
-
- - {content} -
- ); -}); + + + {content} + +
+ ); + }, +); diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index c5b50c0..e315fdc 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -6,28 +6,18 @@ import type { JSONValue, Message } from 'ai'; import React, { type RefCallback, useEffect, useState } from 'react'; import { ClientOnly } from 'remix-utils/client-only'; import { Menu } from '~/components/sidebar/Menu.client'; -import { IconButton } from '~/components/ui/IconButton'; import { Workbench } from '~/components/workbench/Workbench.client'; import { classNames } from '~/utils/classNames'; import { PROVIDER_LIST } from '~/utils/constants'; import { Messages } from './Messages.client'; -import { SendButton } from './SendButton.client'; -import { APIKeyManager, getApiKeysFromCookies } from './APIKeyManager'; +import { getApiKeysFromCookies } from './APIKeyManager'; import Cookies from 'js-cookie'; import * as Tooltip from '@radix-ui/react-tooltip'; - import styles from './BaseChat.module.scss'; -import { ExportChatButton } from '~/components/chat/chatExportAndImport/ExportChatButton'; import { ImportButtons } from '~/components/chat/chatExportAndImport/ImportButtons'; import { ExamplePrompts } from '~/components/chat/ExamplePrompts'; import GitCloneButton from './GitCloneButton'; - -import FilePreview from './FilePreview'; -import { ModelSelector } from '~/components/chat/ModelSelector'; -import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition'; import type { ProviderInfo } from '~/types/model'; -import { ScreenshotStateManager } from './ScreenshotStateManager'; -import { toast } from 'react-toastify'; import StarterTemplates from './StarterTemplates'; import type { ActionAlert, SupabaseAlert, DeployAlert } from '~/types/actions'; import DeployChatAlert from '~/components/deploy/DeployAlert'; @@ -36,13 +26,11 @@ import type { ModelInfo } from '~/lib/modules/llm/types'; import ProgressCompilation from './ProgressCompilation'; import type { ProgressAnnotation } from '~/types/context'; import type { ActionRunner } from '~/lib/runtime/action-runner'; -import { LOCAL_PROVIDERS } from '~/lib/stores/settings'; import { SupabaseChatAlert } from '~/components/chat/SupabaseAlert'; -import { SupabaseConnection } from './SupabaseConnection'; -import { ExpoQrModal } from '~/components/workbench/ExpoQrModal'; import { expoUrlAtom } from '~/lib/stores/qrCodeStore'; import { useStore } from '@nanostores/react'; import { StickToBottom, useStickToBottomContext } from '~/lib/hooks'; +import { ChatBox } from './ChatBox'; const TEXTAREA_MIN_HEIGHT = 76; @@ -82,6 +70,9 @@ interface BaseChatProps { clearDeployAlert?: () => void; data?: JSONValue[] | undefined; actionRunner?: ActionRunner; + chatMode?: 'discuss' | 'build'; + setChatMode?: (mode: 'discuss' | 'build') => void; + append?: (message: Message) => void; } export const BaseChat = React.forwardRef( @@ -120,6 +111,9 @@ export const BaseChat = React.forwardRef( clearSupabaseAlert, data, actionRunner, + chatMode, + setChatMode, + append, }, ref, ) => { @@ -362,6 +356,9 @@ export const BaseChat = React.forwardRef( className="flex flex-col w-full flex-1 max-w-chat pb-6 mx-auto z-1" messages={messages} isStreaming={isStreaming} + append={append} + chatMode={chatMode} + setChatMode={setChatMode} /> ) : null; }} @@ -406,240 +403,44 @@ export const BaseChat = React.forwardRef(
{progressAnnotations && } -
- - - - - - - - - - - - - - - - - - -
- - {() => ( -
- - {(providerList || []).length > 0 && - provider && - (!LOCAL_PROVIDERS.includes(provider.name) || 'OpenAILike') && ( - { - onApiKeysChange(provider.name, key); - }} - /> - )} -
- )} -
-
- { - setUploadedFiles?.(uploadedFiles.filter((_, i) => i !== index)); - setImageDataList?.(imageDataList.filter((_, i) => i !== index)); - }} - /> - - {() => ( - - )} - -
-