Skip to content

Commit

Permalink
feat: add toolbar for message
Browse files Browse the repository at this point in the history
  • Loading branch information
2214962083 committed Nov 20, 2024
1 parent 392875e commit 31b6939
Show file tree
Hide file tree
Showing 16 changed files with 418 additions and 54 deletions.
3 changes: 0 additions & 3 deletions src/extension/ai/model-providers/helpers/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,3 @@ export abstract class BaseModelProvider<LangChainModel extends BaseChatModel> {
: chain
}
}

export const imgUrlForTest =
'https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg'
5 changes: 4 additions & 1 deletion src/extension/ai/model-providers/helpers/feature-tester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import type { AIModel, AIModelFeature } from '@shared/entities/ai-model-entity'
import type { MaybePromise } from 'mermaid/dist/types'
import { z } from 'zod'

import { imgUrlForTest, type BaseModelProvider } from './base'
import { type BaseModelProvider } from './base'

export const imgUrlForTest =
'https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg'

export class FeatureTester {
constructor(private baseModelProvider: BaseModelProvider<BaseChatModel>) {}
Expand Down
25 changes: 25 additions & 0 deletions src/webview/components/chat/chat-ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const ChatUI: FC = () => {
const {
newConversation,
setNewConversation,
deleteConversation,
historiesConversationsWithUIState,
newConversationUIState,
toggleConversationEditMode
Expand Down Expand Up @@ -74,6 +75,28 @@ export const ChatUI: FC = () => {
toggleConversationEditMode(conversation.id, isEditMode)
}

const handleDelete = (conversation: Conversation) => {
// 1. stop generate
// 2. delete conversation
cancelSending()
deleteConversation(conversation.id)
}

const handleRegenerate = async (conversation: Conversation) => {
if (conversation.role !== 'ai') return
cancelSending()

// find the previous conversation
const currentConversationIndex = context.conversations.findIndex(
c => c.id === conversation.id
)
const previousConversation =
context.conversations[currentConversationIndex - 1]
if (!previousConversation) return

await handleSend(previousConversation)
}

const handleContextTypeChange = (value: string) => {
setContext(draft => {
draft.type = value as ChatContextType
Expand Down Expand Up @@ -144,6 +167,8 @@ export const ChatUI: FC = () => {
setContext={setContext}
onSend={handleSend}
onEditModeChange={handleEditModeChange}
onDelete={handleDelete}
onRegenerate={handleRegenerate}
/>
{isSending && (
<div className="absolute left-1/2 bottom-[200px] -translate-x-1/2 z-10">
Expand Down
33 changes: 32 additions & 1 deletion src/webview/components/chat/editor/chat-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type FC,
type Ref
} from 'react'
import { $generateHtmlFromNodes } from '@lexical/html'
import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin'
import {
LexicalComposer,
Expand Down Expand Up @@ -66,6 +67,7 @@ export interface ChatEditorRef {
insertSpaceAndAt: () => void
focusOnEditor: (autoMoveCursorToEnd?: boolean) => void
resetEditor: () => void
copyWithFormatting: () => void
}

export const ChatEditor: FC<ChatEditorProps> = ({
Expand Down Expand Up @@ -179,6 +181,29 @@ const ChatEditorInner: FC<ChatEditorProps> = ({
})
}

const copyWithFormatting = () => {
editor.update(() => {
const htmlString = $generateHtmlFromNodes(editor)
// Create a temporary element to hold the HTML content
const tempElement = document.createElement('div')
tempElement.innerHTML = htmlString

// Create a range and selection
const range = document.createRange()
range.selectNodeContents(tempElement)
const selection = window.getSelection()
selection?.removeAllRanges()
selection?.addRange(range)

// Execute copy command
document.execCommand('copy')

// Clean up
selection?.removeAllRanges()
tempElement.remove()
})
}

useEffect(() => {
if (!autoFocus) return
focusOnEditor()
Expand All @@ -187,7 +212,13 @@ const ChatEditorInner: FC<ChatEditorProps> = ({
useImperativeHandle(
// eslint-disable-next-line react-compiler/react-compiler
ref,
() => ({ editor, insertSpaceAndAt, focusOnEditor, resetEditor }),
() => ({
editor,
insertSpaceAndAt,
focusOnEditor,
resetEditor,
copyWithFormatting
}),
[editor]
)

Expand Down
125 changes: 94 additions & 31 deletions src/webview/components/chat/messages/chat-messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,25 @@ import React, {
useRef,
type CSSProperties,
type FC,
type Ref
type RefObject
} from 'react'
import { getAllTextFromLangchainMessageContents } from '@shared/utils/get-all-text-from-langchain-message-contents'
import { AnimatedList } from '@webview/components/ui/animated-list'
import { ScrollArea } from '@webview/components/ui/scroll-area'
import type { ConversationWithUIState } from '@webview/types/chat'
import { cn } from '@webview/utils/common'
import { cn, copyToClipboard } from '@webview/utils/common'
import scrollIntoView from 'scroll-into-view-if-needed'

import { ChatAIMessage, type ChatAIMessageProps } from './roles/chat-ai-message'
import {
ChatHumanMessage,
type ChatHumanMessageProps
type ChatHumanMessageProps,
type ChatHumanMessageRef
} from './roles/chat-human-message'
import {
MessageToolbar,
type MessageToolbarEvents
} from './toolbars/message-toolbars'

interface ChatMessagesProps
extends Pick<
Expand All @@ -26,6 +32,8 @@ interface ChatMessagesProps
| 'onSend'
| 'className'
| 'style'
| 'onDelete'
| 'onRegenerate'
> {
conversationsWithUIState: ConversationWithUIState[]
}
Expand All @@ -38,10 +46,13 @@ export const ChatMessages: React.FC<ChatMessagesProps> = props => {
onSend,
onEditModeChange,
className,
style
style,
onDelete,
onRegenerate
} = props

const containerRef = useRef<HTMLDivElement>(null)
const scrollContentRef = useRef<HTMLDivElement>(null)
const endOfMessagesRef = useRef<HTMLDivElement>(null)
const prevConversationIdRef = useRef<string>(undefined)

Expand Down Expand Up @@ -99,11 +110,12 @@ export const ChatMessages: React.FC<ChatMessagesProps> = props => {
'chat-messages flex-1 flex flex-col w-full overflow-y-auto gap-2 pt-4',
className
)}
viewPortProps={{ ref: scrollContentRef }}
style={style}
>
<style>
{`
[data-radix-scroll-area-viewport] > div {
.chat-messages [data-radix-scroll-area-viewport] > div {
display:block !important;
width: 100%;
}
Expand All @@ -117,6 +129,7 @@ export const ChatMessages: React.FC<ChatMessagesProps> = props => {
return (
<InnerMessage
key={conversation.id}
scrollContentRef={scrollContentRef}
conversation={conversation}
context={context}
setContext={setContext}
Expand All @@ -125,6 +138,8 @@ export const ChatMessages: React.FC<ChatMessagesProps> = props => {
isLoading={uiState.isLoading}
sendButtonDisabled={uiState.sendButtonDisabled}
onEditModeChange={onEditModeChange}
onDelete={onDelete}
onRegenerate={onRegenerate}
/>
)
})}
Expand All @@ -134,54 +149,102 @@ export const ChatMessages: React.FC<ChatMessagesProps> = props => {
)
}

interface InnerMessageProps extends ChatAIMessageProps, ChatHumanMessageProps {
ref?: Ref<HTMLDivElement>
interface InnerMessageProps
extends ChatAIMessageProps,
Omit<ChatHumanMessageProps, 'ref'>,
Pick<MessageToolbarEvents, 'onDelete' | 'onRegenerate'> {
className?: string
style?: CSSProperties
scrollContentRef: RefObject<HTMLElement | null>
}

const InnerMessage: FC<InnerMessageProps> = props => {
const messageRef = useRef<ChatHumanMessageRef>(null)
const {
ref,
conversation,
onEditModeChange,
context,
setContext,
conversation,
onSend,
isLoading,
isEditMode,
sendButtonDisabled,
onEditModeChange,
className,
style
style,
scrollContentRef,
onDelete,
onRegenerate
} = props

const isAiMessage = conversation.role === 'ai'
const isHumanMessage = conversation.role === 'human'

const handleCopy = () => {
if (isHumanMessage) {
messageRef.current?.copy?.()
}

if (isAiMessage) {
copyToClipboard(
getAllTextFromLangchainMessageContents(conversation.contents)
)
}
}

const renderMessageToolbar = () => (
<MessageToolbar
conversation={conversation}
scrollContentRef={scrollContentRef}
messageRef={messageRef}
onEdit={
isHumanMessage
? () => onEditModeChange?.(true, conversation)
: undefined
}
onCopy={handleCopy}
onDelete={onDelete}
onRegenerate={isAiMessage ? onRegenerate : undefined}
/>
)

return (
<div
key={conversation.id}
ref={ref}
className={cn('flex relative max-w-full w-full items-center', className)}
className={cn(
'flex flex-col relative max-w-full w-full items-start px-4',
conversation.role === 'human' && 'items-end',
className
)}
style={style}
>
{conversation.role === 'ai' && (
<ChatAIMessage
conversation={conversation}
isLoading={isLoading}
isEditMode={isEditMode}
onEditModeChange={onEditModeChange}
/>
{isAiMessage && (
<>
<ChatAIMessage
ref={messageRef}
conversation={conversation}
isLoading={isLoading}
isEditMode={isEditMode}
// onEditModeChange={onEditModeChange}
/>
{renderMessageToolbar()}
</>
)}

{conversation.role === 'human' && (
<ChatHumanMessage
context={context}
setContext={setContext}
conversation={conversation}
onSend={onSend}
isLoading={isLoading}
isEditMode={isEditMode}
sendButtonDisabled={sendButtonDisabled}
onEditModeChange={onEditModeChange}
/>
{isHumanMessage && (
<>
<ChatHumanMessage
ref={messageRef}
context={context}
setContext={setContext}
conversation={conversation}
onSend={onSend}
isLoading={isLoading}
isEditMode={isEditMode}
sendButtonDisabled={sendButtonDisabled}
onEditModeChange={onEditModeChange}
/>
{renderMessageToolbar()}
</>
)}
</div>
)
Expand Down
12 changes: 6 additions & 6 deletions src/webview/components/chat/messages/roles/chat-ai-message.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CSSProperties, FC } from 'react'
import type { CSSProperties, FC, Ref } from 'react'
import type { Conversation } from '@shared/entities'
import { getAllTextFromLangchainMessageContents } from '@shared/utils/get-all-text-from-langchain-message-contents'
import { WithPluginRegistryProvider } from '@webview/contexts/plugin-registry-context'
Expand All @@ -9,6 +9,7 @@ import { Markdown } from '../markdown'
import { ChatAIMessageLogAccordion } from './chat-log-preview'

export interface ChatAIMessageProps extends ConversationUIState {
ref?: Ref<HTMLDivElement>
className?: string
style?: CSSProperties
conversation: Conversation
Expand All @@ -17,6 +18,7 @@ export interface ChatAIMessageProps extends ConversationUIState {

const _ChatAIMessage: FC<ChatAIMessageProps> = props => {
const {
ref,
conversation,
isLoading,
className,
Expand All @@ -26,18 +28,17 @@ const _ChatAIMessage: FC<ChatAIMessageProps> = props => {
} = props

return (
<div className="w-full flex">
<div ref={ref} className="w-full flex">
<div
className={cn(
'ml-4 mr-auto relative bg-background text-foreground border overflow-hidden rounded-md rounded-bl-[0px]',
'mr-auto relative bg-background text-foreground border overflow-hidden rounded-md rounded-bl-[0px]',
isEditMode && 'w-full',
className
)}
style={style}
onClick={() => {
if (isEditMode) return
console.log('edit mode', onEditModeChange)
// onEditModeChange?.(true, conversation)
onEditModeChange?.(true, conversation)
}}
>
{conversation.logs.length > 0 && (
Expand All @@ -55,7 +56,6 @@ const _ChatAIMessage: FC<ChatAIMessageProps> = props => {
{getAllTextFromLangchainMessageContents(conversation.contents)}
</Markdown>
</div>
<div className="w-4 shrink-0" />
</div>
)
}
Expand Down
Loading

0 comments on commit 31b6939

Please sign in to comment.