From 9d9954c90eb77f96cc29ccf7016abb3bf27d280f Mon Sep 17 00:00:00 2001 From: Paul Ccari <46382556+paulclindo@users.noreply.github.com> Date: Tue, 10 Dec 2024 19:43:19 -0500 Subject: [PATCH] feat: drag n drop files + refactor markdown (#550) * wip * fix: markdown pre, code elements * fixes * fixes * refactor * remove uiw markdown dependency * fixes * fixes * fixes * feat: drag n drop files in textarea box * fix: npm audit --- apps/shinkai-desktop/package.json | 2 +- .../file-selection-action-bar.tsx | 48 + .../components/chat/components/message.tsx | 14 +- .../components/chat/conversation-footer.tsx | 921 +- apps/shinkai-desktop/src/globals.css | 5 + .../src/pages/prompt-library.tsx | 10 +- .../spotlight/components/quick-ask.tsx | 16 +- .../src/components/chat/message.tsx | 40 +- .../src/components/markdown-preview.tsx | 593 +- libs/shinkai-ui/src/styles/styles.css | 3 + package-lock.json | 16153 +++++++++------- package.json | 7 +- 12 files changed, 10403 insertions(+), 7409 deletions(-) create mode 100644 apps/shinkai-desktop/src/components/chat/chat-action-bar/file-selection-action-bar.tsx diff --git a/apps/shinkai-desktop/package.json b/apps/shinkai-desktop/package.json index 3764f407e..fc6fef9cc 100644 --- a/apps/shinkai-desktop/package.json +++ b/apps/shinkai-desktop/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build", + "build": "tsc && NODE_OPTIONS=\\\"--max-old-space-size=8192\\\" vite build ", "preview": "vite preview", "lint": "eslint --ext .js,.jsx,.ts,.tsx . --ignore-path .gitignore", "tauri": "tauri", diff --git a/apps/shinkai-desktop/src/components/chat/chat-action-bar/file-selection-action-bar.tsx b/apps/shinkai-desktop/src/components/chat/chat-action-bar/file-selection-action-bar.tsx new file mode 100644 index 000000000..78d14a11a --- /dev/null +++ b/apps/shinkai-desktop/src/components/chat/chat-action-bar/file-selection-action-bar.tsx @@ -0,0 +1,48 @@ +import { useTranslation } from '@shinkai_network/shinkai-i18n'; +import { + Tooltip, + TooltipContent, + TooltipPortal, + TooltipTrigger, +} from '@shinkai_network/shinkai-ui'; +import { cn } from '@shinkai_network/shinkai-ui/utils'; +import { Paperclip } from 'lucide-react'; +import * as React from 'react'; + +import { allowedFileExtensions } from '../../../lib/constants'; +import { actionButtonClassnames } from '../conversation-footer'; + +type FileUploadInputProps = { + inputProps: React.InputHTMLAttributes; + onClick: () => void; +}; + +export function FileSelectionActionBar({ + onClick, + inputProps, +}: FileUploadInputProps) { + const { t } = useTranslation(); + + return ( + <> + + + + + + + {t('common.uploadFile')}
+ {allowedFileExtensions.join(', ')} +
+
+
+ + + ); +} diff --git a/apps/shinkai-desktop/src/components/chat/components/message.tsx b/apps/shinkai-desktop/src/components/chat/components/message.tsx index 8e1cfd9c7..8916ef681 100644 --- a/apps/shinkai-desktop/src/components/chat/components/message.tsx +++ b/apps/shinkai-desktop/src/components/chat/components/message.tsx @@ -21,7 +21,7 @@ import { FileList, Form, FormField, - MarkdownPreview, + MarkdownText, PythonCodeRunner, Tooltip, TooltipContent, @@ -339,16 +339,14 @@ export const MessageBase = ({ )} {message.role === 'assistant' && ( - )} {message.role === 'assistant' && diff --git a/apps/shinkai-desktop/src/components/chat/conversation-footer.tsx b/apps/shinkai-desktop/src/components/chat/conversation-footer.tsx index 98ce62097..11b6ed836 100644 --- a/apps/shinkai-desktop/src/components/chat/conversation-footer.tsx +++ b/apps/shinkai-desktop/src/components/chat/conversation-footer.tsx @@ -1,5 +1,5 @@ import { zodResolver } from '@hookform/resolvers/zod'; -import { StopIcon } from '@radix-ui/react-icons'; +import { PlusCircledIcon, StopIcon } from '@radix-ui/react-icons'; import { useTranslation } from '@shinkai_network/shinkai-i18n'; import { buildInboxIdFromJobId, @@ -62,7 +62,6 @@ import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { toast } from 'sonner'; import { useGetCurrentInbox } from '../../hooks/use-current-inbox'; -import { allowedFileExtensions } from '../../lib/constants'; import { useAnalytics } from '../../lib/posthog-provider'; import { useAuth } from '../../store/auth'; import { useSettings } from '../../store/settings'; @@ -77,6 +76,7 @@ import { CreateChatConfigActionBar, UpdateChatConfigActionBar, } from './chat-action-bar/chat-config-action-bar'; +import { FileSelectionActionBar } from './chat-action-bar/file-selection-action-bar'; import PromptSelectionActionBar from './chat-action-bar/prompt-selection-action-bar'; import { streamingSupportedModels } from './constants'; import { useSetJobScope } from './context/set-job-scope-context'; @@ -94,7 +94,6 @@ export type ChatConversationLocationState = { function ConversationEmptyFooter() { const { t } = useTranslation(); - const size = partial({ standard: 'jedec' }); const navigate = useNavigate(); const { inboxId: encodedInboxId = '' } = useParams(); const inboxId = decodeURIComponent(encodedInboxId); @@ -239,15 +238,21 @@ function ConversationEmptyFooter() { }, ); - const { getRootProps: getRootFileProps, getInputProps: getInputFileProps } = - useDropzone({ - multiple: true, - onDrop: (acceptedFiles) => { - const previousFiles = chatForm.getValues('files') ?? []; - const newFiles = [...previousFiles, ...acceptedFiles]; - chatForm.setValue('files', newFiles, { shouldValidate: true }); - }, - }); + const { + getRootProps: getRootFileProps, + getInputProps: getInputFileProps, + isDragActive, + open: openFilePicker, + } = useDropzone({ + noClick: true, + noKeyboard: true, + multiple: true, + onDrop: (acceptedFiles) => { + const previousFiles = chatForm.getValues('files') ?? []; + const newFiles = [...previousFiles, ...acceptedFiles]; + chatForm.setValue('files', newFiles, { shouldValidate: true }); + }, + }); const currentFiles = useWatch({ control: chatForm.control, @@ -335,235 +340,200 @@ function ConversationEmptyFooter() { }; return ( -
-
-
- ( - - - {t('chat.enterMessage')} - - -
-
-
- { - chatForm.setValue('agent', value); - }} - value={chatForm.watch('agent')} - /> - - - -
- - -
-
- - - {t('common.uploadFile')}
- {allowedFileExtensions.join(', ')} -
-
-
- - +
+
+
+ + ( + + + {t('chat.enterMessage')} + + +
+
+
+ { + chatForm.setValue('agent', value); + }} + value={chatForm.watch('agent')} + /> + + + + +
+ {!isAgentInbox && ( + + )}
- {!isAgentInbox && ( - - )} -
- - {!debounceMessage && ( - - Enter to send - - )} - -
- } - disabled={isPending} - onChange={field.onChange} - onSubmit={chatForm.handleSubmit(onSubmit)} - topAddons={ - <> - {selectedTool && ( -
- - -
- -
- - {formatText(selectedTool.name)}{' '} - - + +
+ } + disabled={isPending} + onChange={field.onChange} + onSubmit={chatForm.handleSubmit(onSubmit)} + topAddons={ + <> + {isDragActive && } + {selectedTool && ( +
+ + +
+ +
+ + {formatText(selectedTool.name)}{' '} + + +
-
- - - - {selectedTool.description} - - - - - -
- )} - {currentFiles && currentFiles.length > 0 && ( -
- {currentFiles.map((file, index) => ( -
+ + + {selectedTool.description} + + + + + +
+ )} + {!isDragActive && + currentFiles && + currentFiles.length > 0 && ( + { + const newFiles = [...currentFiles]; + newFiles.splice(index, 1); + chatForm.setValue('files', newFiles, { + shouldValidate: true, + }); + }} + /> + )} + + } + value={field.value} + /> + +
+ {!!debounceMessage && + !selectedTool && + isSearchToolListSuccess && + searchToolList?.length > 0 && + searchToolList?.map((tool) => ( + + + { - event.stopPropagation(); - const newFiles = [...currentFiles]; - newFiles.splice(index, 1); - chatForm.setValue('files', newFiles, { - shouldValidate: true, + exit={{ opacity: 0, x: -10 }} + initial={{ opacity: 0, x: -10 }} + key={tool.tool_router_key} + onClick={() => { + chatForm.setValue('tool', { + key: tool.tool_router_key, + name: tool.name, + description: tool.description, }); }} + type="button" > - - -
- ))} -
+ + {formatText(tool.name)} + +
+ + + {tool.description} + + +
+ ))} + {!debounceMessage && ( + + Shift + Enter{' '} + for a new line + )} - - } - value={field.value} - /> - -
- {!!debounceMessage && - !selectedTool && - isSearchToolListSuccess && - searchToolList?.length > 0 && - searchToolList?.map((tool) => ( - - - { - chatForm.setValue('tool', { - key: tool.tool_router_key, - name: tool.name, - description: tool.description, - }); - }} - type="button" - > - - {formatText(tool.name)} - - - - - {tool.description} - - - - ))} - {!debounceMessage && ( - - Shift + Enter{' '} - for a new line - - )} -
-
-
- - - )} - /> - +
+ +
+ + + )} + /> + +
); @@ -571,7 +541,6 @@ function ConversationEmptyFooter() { function ConversationChatFooter({ inboxId }: { inboxId: string }) { const { t } = useTranslation(); - const size = partial({ standard: 'jedec' }); const textareaRef = useRef(null); const auth = useAuth((state) => state.auth); @@ -636,15 +605,21 @@ function ConversationChatFooter({ inboxId }: { inboxId: string }) { }, ); - const { getRootProps: getRootFileProps, getInputProps: getInputFileProps } = - useDropzone({ - multiple: true, - onDrop: (acceptedFiles) => { - const previousFiles = chatForm.getValues('files') ?? []; - const newFiles = [...previousFiles, ...acceptedFiles]; - chatForm.setValue('files', newFiles, { shouldValidate: true }); - }, - }); + const { + getRootProps: getRootFileProps, + getInputProps: getInputFileProps, + isDragActive, + open: openFilePicker, + } = useDropzone({ + noClick: true, + noKeyboard: true, + multiple: true, + onDrop: (acceptedFiles) => { + const previousFiles = chatForm.getValues('files') ?? []; + const newFiles = [...previousFiles, ...acceptedFiles]; + chatForm.setValue('files', newFiles, { shouldValidate: true }); + }, + }); const currentFiles = useWatch({ control: chatForm.control, @@ -727,236 +702,195 @@ function ConversationChatFooter({ inboxId }: { inboxId: string }) { }, [chatForm, inboxId]); return ( -
-
- -
- ( - - - {t('chat.enterMessage')} - - -
-
-
- - - - -
- - -
-
- - - {t('common.uploadFile')} -
- {allowedFileExtensions.join(', ')} -
-
-
- - -
+
+
+
+ + + ( + + + {t('chat.enterMessage')} + + +
+
+
+ + + +
- {!isAgentInbox && } -
+ {!isAgentInbox && } +
- - {!debounceMessage && ( - - Enter to send - - )} - -
- } - disabled={isLoadingMessage} - onChange={field.onChange} - onSubmit={chatForm.handleSubmit(onSubmit)} - ref={textareaRef} - topAddons={ - <> - {selectedTool && ( -
- - -
- -
- - {formatText(selectedTool.name)}{' '} - - + +
+ } + disabled={isLoadingMessage} + onChange={field.onChange} + onSubmit={chatForm.handleSubmit(onSubmit)} + ref={textareaRef} + topAddons={ + <> + {isDragActive && } + {selectedTool && ( +
+ + +
+ +
+ + {formatText(selectedTool.name)}{' '} + + +
-
- - - - {selectedTool.description} - - - - - -
- )} - {currentFiles && currentFiles.length > 0 && ( -
- {currentFiles.map((file, index) => ( -
+ + + {selectedTool.description} + + + + + +
+ )} + {!isDragActive && + currentFiles && + currentFiles.length > 0 && ( + { + const newFiles = [...currentFiles]; + newFiles.splice(index, 1); + chatForm.setValue('files', newFiles, { + shouldValidate: true, + }); + }} + /> + )} + + } + value={field.value} + /> + +
+ {!!debounceMessage && + !selectedTool && + isSearchToolListSuccess && + searchToolList?.length > 0 && + searchToolList?.map((tool) => ( + + + { - event.stopPropagation(); - const newFiles = [...currentFiles]; - newFiles.splice(index, 1); - chatForm.setValue('files', newFiles, { - shouldValidate: true, + exit={{ opacity: 0, x: -10 }} + initial={{ opacity: 0, x: -10 }} + key={tool.tool_router_key} + onClick={() => { + chatForm.setValue('tool', { + key: tool.tool_router_key, + name: tool.name, + description: tool.description, }); }} + type="button" > - - -
- ))} -
+ + {formatText(tool.name)} + +
+ + + {tool.description} + + +
+ ))} + {!debounceMessage && ( + + Shift + Enter{' '} + for a new line + )} - - } - value={field.value} - /> - -
- {!!debounceMessage && - !selectedTool && - isSearchToolListSuccess && - searchToolList?.length > 0 && - searchToolList?.map((tool) => ( - - - { - chatForm.setValue('tool', { - key: tool.tool_router_key, - name: tool.name, - description: tool.description, - }); - }} - type="button" - > - - {formatText(tool.name)} - - - - - {tool.description} - - - - ))} - {!debounceMessage && ( - - Shift + Enter{' '} - for a new line - - )} -
-
-
- - - )} - /> - +
+ +
+ + + )} + /> + +
); @@ -1010,3 +944,72 @@ function StopGeneratingButton({ ); } + +type FileListProps = { + currentFiles: File[]; + onRemoveFile: (index: number) => void; +}; + +const FileList = ({ currentFiles, onRemoveFile }: FileListProps) => { + const size = partial({ standard: 'jedec' }); + + return ( +
+
+ {currentFiles.map((file, index) => ( +
+
+ {getFileExt(file.name) && fileIconMap[getFileExt(file.name)] ? ( + + ) : ( + + )} +
+ +
+ {file.name} + + {size(file.size)} + +
+ +
+ ))} +
+
+ ); +}; + +const DropFileActive = () => ( + +
+
+ + + Drop file here to add to your conversation + +
+
+
+); diff --git a/apps/shinkai-desktop/src/globals.css b/apps/shinkai-desktop/src/globals.css index f6e8ad4f2..e0f3efa25 100644 --- a/apps/shinkai-desktop/src/globals.css +++ b/apps/shinkai-desktop/src/globals.css @@ -25,3 +25,8 @@ html { ::-webkit-scrollbar-track { @apply bg-gray-400; } + +.no-scrollbar { + scrollbar-width: none; + -ms-overflow-style: none; +} diff --git a/apps/shinkai-desktop/src/pages/prompt-library.tsx b/apps/shinkai-desktop/src/pages/prompt-library.tsx index 1ab7c2750..5c5b4ca32 100644 --- a/apps/shinkai-desktop/src/pages/prompt-library.tsx +++ b/apps/shinkai-desktop/src/pages/prompt-library.tsx @@ -11,7 +11,7 @@ import { Button, CopyToClipboardIcon, Input, - MarkdownPreview, + MarkdownText, ScrollArea, Textarea, Tooltip, @@ -367,9 +367,9 @@ function PromptPreview({ setPromptEditContent={setPromptEditContent} /> ) : ( - )}
@@ -536,9 +536,9 @@ export const PromptTryOut = ({ Output

-
diff --git a/apps/shinkai-desktop/src/windows/spotlight/components/quick-ask.tsx b/apps/shinkai-desktop/src/windows/spotlight/components/quick-ask.tsx index e6143c5e7..ffc4502e5 100644 --- a/apps/shinkai-desktop/src/windows/spotlight/components/quick-ask.tsx +++ b/apps/shinkai-desktop/src/windows/spotlight/components/quick-ask.tsx @@ -14,7 +14,7 @@ import { CollapsibleTrigger, CopyToClipboardIcon, DotsLoader, - MarkdownPreview, + MarkdownText, ScrollArea, Separator, } from '@shinkai_network/shinkai-ui'; @@ -409,9 +409,9 @@ const QuickAskBodyWithResponseBase = ({ - @@ -421,14 +421,11 @@ const QuickAskBodyWithResponseBase = ({ lastMessage?.content === '' && } {lastMessage?.role === 'assistant' && ( - )}
diff --git a/libs/shinkai-ui/src/components/chat/message.tsx b/libs/shinkai-ui/src/components/chat/message.tsx index e7e385d7f..25c12cd82 100644 --- a/libs/shinkai-ui/src/components/chat/message.tsx +++ b/libs/shinkai-ui/src/components/chat/message.tsx @@ -30,7 +30,7 @@ import { Card, CardContent } from '../card'; import { CopyToClipboardIcon } from '../copy-to-clipboard-icon'; import { DotsLoader } from '../dots-loader'; import { Form, FormField } from '../form'; -import { MarkdownPreview } from '../markdown-preview'; +import { MarkdownText, MarkdownTextPrimitive } from '../markdown-preview'; import { Tooltip, TooltipContent, @@ -343,42 +343,14 @@ const MessageBase = ({ )} {message.role === 'assistant' && ( - ( - { - if (artifact?.identifier === messageId) { - setArtifact?.(null); - return; - } - const selectedArtifact = artifacts?.find( - (art) => art.identifier === messageId, - ); - setArtifact?.(selectedArtifact ?? null); - }} - title={title} - type={type} - /> - ), - }} - source={extractErrorPropertyOrContent( + )} diff --git a/libs/shinkai-ui/src/components/markdown-preview.tsx b/libs/shinkai-ui/src/components/markdown-preview.tsx index 8d25e7dbb..c3f7ed88b 100644 --- a/libs/shinkai-ui/src/components/markdown-preview.tsx +++ b/libs/shinkai-ui/src/components/markdown-preview.tsx @@ -1,84 +1,533 @@ -import ReactMarkdownPreview from '@uiw/react-markdown-preview'; -import React from 'react'; -import rehypeRewrite, { RehypeRewriteOptions } from 'rehype-rewrite'; -import { PluggableList } from 'unified'; +import { Primitive } from '@radix-ui/react-primitive'; +import { useCallbackRef } from '@radix-ui/react-use-callback-ref'; +import React, { + ComponentPropsWithoutRef, + ComponentType, + createContext, + ElementRef, + ElementType, + FC, + forwardRef, + ForwardRefExoticComponent, + ReactNode, + RefAttributes, + useContext, + useMemo, +} from 'react'; +import ReactMarkdown, { type Options } from 'react-markdown'; +import { + PrismAsyncLight, + SyntaxHighlighterProps as SHP, +} from 'react-syntax-highlighter'; +import python from 'react-syntax-highlighter/dist/esm/languages/prism/python'; +import tsx from 'react-syntax-highlighter/dist/esm/languages/prism/tsx'; +import { default as github } from 'react-syntax-highlighter/dist/esm/styles/hljs/github'; +// import rehypeRaw from 'rehype-raw'; +import remarkGfm from 'remark-gfm'; import { cn } from '../utils'; +import { CopyToClipboardIcon } from './copy-to-clipboard-icon'; -// TODO: remove @uiw/react-markdown-preview dependency to use the main library react-markdown for better control - -const rehypePlugins: PluggableList = [ - [ - rehypeRewrite, - { - rewrite: (node, _, parent) => { - if ( - node.type === 'element' && - node.tagName === 'a' && - parent && - parent.type === 'element' && - /^h([123456])/.test(parent.tagName) - ) { - parent.children = [parent.children[1]]; - } - }, - } as RehypeRewriteOptions, - ], -]; - -export const MarkdownPreview = ({ - className, - source, - components, -}: { - className?: string; - source?: string; - components?: Parameters[0]['components']; -}) => { +type CodeHeaderProps = { + language: string | undefined; + code: string; +}; +export const CodeHeader: FC = ({ language, code }) => { return ( - + + {language} + + svg]:h-3 [&>svg]:w-3', + )} + string={code} + /> +
+ ); +}; + +const makeMakeSyntaxHighlighter = + (SyntaxHighlighter: ComponentType) => + (config: Omit) => { + const PrismSyntaxHighlighter: FC = ({ + components: { Pre, Code }, + language, + code, + }) => { + return ( + + {code} + + ); + }; + + PrismSyntaxHighlighter.displayName = 'PrismSyntaxHighlighter'; + + return PrismSyntaxHighlighter; + }; + +// register languages you want to support +PrismAsyncLight.registerLanguage('js', tsx); +PrismAsyncLight.registerLanguage('jsx', tsx); +PrismAsyncLight.registerLanguage('ts', tsx); +PrismAsyncLight.registerLanguage('tsx', tsx); +PrismAsyncLight.registerLanguage('python', python); + +export const makePrismAsyncLightSyntaxHighlighter = + makeMakeSyntaxHighlighter(PrismAsyncLight); + +export const SyntaxHighlighterBase = makePrismAsyncLightSyntaxHighlighter({ + style: github, + customStyle: { + margin: 0, + width: '100%', + background: '#0d1117', + padding: '1.5rem 1rem', + fontSize: '0.75rem', + }, +}); + +export const defaultComponents: MarkdownTextPrimitiveProps['components'] = { + h1: ({ node, className, ...props }) => ( +

+ ), + h2: ({ node, className, ...props }) => ( +

+ ), + h3: ({ node, className, ...props }) => ( +

+ ), + h4: ({ node, className, ...props }) => ( +

+ ), + h5: ({ node, className, ...props }) => ( +

+ ), + h6: ({ node, className, ...props }) => ( +
+ ), + p: ({ node, className, ...props }) => ( +

+ ), + a: ({ node, className, ...props }) => ( + + ), + blockquote: ({ node, className, ...props }) => ( +

+ ), + ul: ({ node, className, ...props }) => ( +