Skip to content

Commit 15012fa

Browse files
committed
More component cleanup
1 parent e24fc3a commit 15012fa

File tree

6 files changed

+154
-215
lines changed

6 files changed

+154
-215
lines changed

webview-ui/src/components/research/Chat.tsx

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import { cn } from "@/lib/utils"
44

55
import { type ChatHandler } from "./types"
66
import { ChatProvider } from "./providers/ChatProvider"
7-
import ChatInput from "./ChatInput"
8-
import ChatMessages from "./ChatMessages"
7+
import { ChatMessagesProvider } from "./providers/ChatMessagesProvider"
8+
import { ChatMessages, ChatActions } from "./ChatMessages"
9+
import { ChatInput } from "./ChatInput"
10+
import { useChatUI } from "./hooks"
911

1012
type ChatProps = {
1113
handler: ChatHandler
@@ -16,14 +18,31 @@ export const Chat = ({ handler }: ChatProps) => {
1618

1719
return (
1820
<ChatProvider value={{ ...handler, requestData, setRequestData }}>
19-
<div className={cn("flex flex-col h-[100vh]")}>
20-
<div className="flex-1 overflow-auto">
21-
<ChatMessages />
22-
</div>
23-
<div className="sticky bottom-0 border-t">
24-
<ChatInput />
25-
</div>
26-
</div>
21+
<ChatComponent />
2722
</ChatProvider>
2823
)
2924
}
25+
26+
const ChatComponent = () => {
27+
const { messages, reload, stop, isLoading } = useChatUI()
28+
29+
const messageLength = messages.length
30+
const lastMessage = messages[messageLength - 1]
31+
const isLastMessageFromAssistant = messageLength > 0 && lastMessage?.role !== "user"
32+
const showReload = reload && !isLoading && isLastMessageFromAssistant
33+
const showStop = stop && isLoading
34+
35+
// The `isPending` flag indicates that stream response is not yet received
36+
// from the server, so we show a loading indicator to give a better UX.
37+
const isPending = isLoading && !isLastMessageFromAssistant
38+
39+
return (
40+
<ChatMessagesProvider value={{ isPending, showReload, showStop, lastMessage, messageLength }}>
41+
<div className={cn("relative flex flex-col flex-1 min-h-0 pt-2 pr-[1px]")}>
42+
<ChatMessages />
43+
<ChatActions />
44+
<ChatInput />
45+
</div>
46+
</ChatMessagesProvider>
47+
)
48+
}

webview-ui/src/components/research/ChatInput.tsx

Lines changed: 33 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { Button, AutosizeTextarea } from "@/components/ui"
22

33
import { Message } from "./types"
4-
import { FileUploader } from "./widgets/FileUploader"
4+
// import { FileUploader } from "./widgets/FileUploader"
55
import { ChatInputProvider } from "./providers/ChatInputProvider"
6-
import { useChatUI } from "./hooks/useChatUI"
7-
import { useChatInput } from "./hooks/useChatInput"
6+
import { useChatUI, useChatInput } from "./hooks"
87

98
/**
109
* ChatInput
@@ -15,7 +14,7 @@ type ChatInputProps = {
1514
annotations?: any
1615
}
1716

18-
function ChatInput({ annotations, resetUploadedFiles }: ChatInputProps) {
17+
export function ChatInput({ annotations, resetUploadedFiles }: ChatInputProps) {
1918
const { input, setInput, append, isLoading, requestData } = useChatUI()
2019
const isDisabled = isLoading || !input.trim()
2120

@@ -50,7 +49,7 @@ function ChatInput({ annotations, resetUploadedFiles }: ChatInputProps) {
5049

5150
return (
5251
<ChatInputProvider value={{ isDisabled, handleKeyDown, handleSubmit }}>
53-
<div className="flex shrink-0 flex-col gap-4 p-4">
52+
<div className="p-3">
5453
<ChatInputForm />
5554
</div>
5655
</ChatInputProvider>
@@ -80,47 +79,49 @@ interface ChatInputFieldProps {
8079
placeholder?: string
8180
}
8281

83-
function ChatInputField({ placeholder = "Type a message" }: ChatInputFieldProps) {
82+
function ChatInputField({ placeholder = "What do you want to research?" }: ChatInputFieldProps) {
8483
const { input, setInput } = useChatUI()
8584
const { handleKeyDown } = useChatInput()
8685

8786
return (
8887
<AutosizeTextarea
8988
name="input"
9089
placeholder={placeholder}
91-
// className="h-[40px] min-h-0 flex-1"
90+
minHeight={75}
91+
maxHeight={200}
9292
value={input}
9393
onChange={({ target: { value } }) => setInput(value)}
9494
onKeyDown={handleKeyDown}
95+
className="resize-none px-3 pt-3 pb-[50px]"
9596
/>
9697
)
9798
}
9899

99100
/**
100-
* ChatInput
101+
* ChatInputUpload
101102
*/
102103

103-
const ALLOWED_EXTENSIONS = ["png", "jpg", "jpeg", "csv", "pdf", "txt", "docx"]
104+
// const ALLOWED_EXTENSIONS = ["png", "jpg", "jpeg", "csv", "pdf", "txt", "docx"]
104105

105-
interface ChatInputUploadProps {
106-
onUpload?: (file: File) => Promise<void> | undefined
107-
allowedExtensions?: string[]
108-
multiple?: boolean
109-
}
106+
// interface ChatInputUploadProps {
107+
// onUpload?: (file: File) => Promise<void> | undefined
108+
// allowedExtensions?: string[]
109+
// multiple?: boolean
110+
// }
110111

111-
function ChatInputUpload({ onUpload, allowedExtensions = ALLOWED_EXTENSIONS, multiple = true }: ChatInputUploadProps) {
112-
const { requestData, setRequestData, isLoading } = useChatUI()
112+
// function ChatInputUpload({ onUpload, allowedExtensions = ALLOWED_EXTENSIONS, multiple = true }: ChatInputUploadProps) {
113+
// const { requestData, setRequestData, isLoading } = useChatUI()
113114

114-
const onFileUpload = async (file: File) => {
115-
if (onUpload) {
116-
await onUpload(file)
117-
} else {
118-
setRequestData({ ...(requestData || {}), file })
119-
}
120-
}
115+
// const onFileUpload = async (file: File) => {
116+
// if (onUpload) {
117+
// await onUpload(file)
118+
// } else {
119+
// setRequestData({ ...(requestData || {}), file })
120+
// }
121+
// }
121122

122-
return <FileUploader onFileUpload={onFileUpload} config={{ disabled: isLoading, allowedExtensions, multiple }} />
123-
}
123+
// return <FileUploader onFileUpload={onFileUpload} config={{ disabled: isLoading, allowedExtensions, multiple }} />
124+
// }
124125

125126
/**
126127
* ChatInputSubmit
@@ -130,15 +131,12 @@ function ChatInputSubmit() {
130131
const { isDisabled } = useChatInput()
131132

132133
return (
133-
<Button variant="ghost" type="submit" disabled={isDisabled}>
134-
<span className="codicon codicon-send" />
135-
</Button>
134+
<div className="absolute bottom-[1px] left-[1px] right-[1px] h-[40px] bg-input border-t border-vscode-editor-background rounded-b-md">
135+
<div className="flex flex-row-reverse items-center gap-2">
136+
<Button type="submit" variant="ghost" size="icon" disabled={isDisabled}>
137+
<span className="codicon codicon-send" />
138+
</Button>
139+
</div>
140+
</div>
136141
)
137142
}
138-
139-
ChatInput.Form = ChatInputForm
140-
ChatInput.Field = ChatInputField
141-
ChatInput.Upload = ChatInputUpload
142-
ChatInput.Submit = ChatInputSubmit
143-
144-
export default ChatInput
Lines changed: 46 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { Bot, Check, Copy, MessageCircle, User2 } from "lucide-react"
2-
import { Fragment, memo, useMemo } from "react"
1+
import { Fragment, useMemo } from "react"
2+
import { CopyIcon, CheckIcon } from "@radix-ui/react-icons"
3+
import { BrainCircuit, CircleUserRound } from "lucide-react"
34

45
import { cn } from "@/lib/utils"
5-
import { Button } from "@/components/ui"
66

77
import { ChatHandler, Message } from "./types"
88
import { ChatMessageProvider } from "./providers/ChatMessageProvider"
@@ -23,26 +23,25 @@ import {
2323
* ChatMessage
2424
*/
2525

26-
interface ChatMessageProps extends React.PropsWithChildren {
27-
message: Message
28-
isLast: boolean
29-
className?: string
26+
interface ChatMessageProps {
3027
isLoading?: boolean
28+
isLast: boolean
29+
message: Message
3130
append?: ChatHandler["append"]
3231
}
3332

34-
function ChatMessage(props: ChatMessageProps) {
35-
const children = props.children ?? (
36-
<>
37-
<ChatMessageAvatar />
38-
<ChatMessageContent isLoading={props.isLoading} append={props.append} />
39-
<ChatMessageActions />
40-
</>
41-
)
42-
33+
export function ChatMessage({ isLoading, isLast, message, append }: ChatMessageProps) {
4334
return (
44-
<ChatMessageProvider value={{ message: props.message, isLast: props.isLast }}>
45-
<div className={cn("group flex gap-4 p-3", props.className)}>{children}</div>
35+
<ChatMessageProvider value={{ message, isLast }}>
36+
<div
37+
className={cn("relative group flex", {
38+
"flex-row-reverse": message.role === "user",
39+
"bg-vscode-input-background/50": message.role === "user",
40+
})}>
41+
<ChatMessageAvatar />
42+
<ChatMessageContent isLoading={isLoading} append={append} />
43+
<ChatMessageActions />
44+
</div>
4645
</ChatMessageProvider>
4746
)
4847
}
@@ -51,25 +50,17 @@ function ChatMessage(props: ChatMessageProps) {
5150
* ChatMessageAvatar
5251
*/
5352

54-
interface ChatMessageAvatarProps extends React.PropsWithChildren {
55-
className?: string
56-
}
57-
58-
function ChatMessageAvatar(props: ChatMessageAvatarProps) {
53+
function ChatMessageAvatar() {
5954
const { message } = useChatMessage()
6055

6156
const roleIconMap: Record<string, React.ReactNode> = {
62-
user: <User2 className="h-4 w-4" />,
63-
assistant: <Bot className="h-4 w-4" />,
57+
user: <CircleUserRound className="h-4 w-4" />,
58+
assistant: <BrainCircuit className="h-4 w-4" />,
6459
}
6560

66-
const children = props.children ?? roleIconMap[message.role] ?? <MessageCircle className="h-4 w-4" />
67-
68-
return (
69-
<div className="bg-background flex h-8 w-8 shrink-0 select-none items-center justify-center border">
70-
{children}
71-
</div>
72-
)
61+
return roleIconMap[message.role] ? (
62+
<div className="shrink-0 opacity-25 select-none p-2">{roleIconMap[message.role]}</div>
63+
) : null
7364
}
7465

7566
/**
@@ -101,25 +92,22 @@ type ContentDisplayConfig = {
10192
component: React.ReactNode | null
10293
}
10394

104-
interface ChatMessageContentProps extends React.PropsWithChildren {
105-
className?: string
106-
content?: ContentDisplayConfig[]
95+
interface ChatMessageContentProps {
10796
isLoading?: boolean
97+
content?: ContentDisplayConfig[]
10898
append?: ChatHandler["append"]
109-
message?: Message // in case you want to customize the message
11099
}
111100

112-
function ChatMessageContent(props: ChatMessageContentProps) {
113-
const { message: defaultMessage, isLast } = useChatMessage()
114-
const message = props.message ?? defaultMessage
101+
function ChatMessageContent({ isLoading, content, append }: ChatMessageContentProps) {
102+
const { message, isLast } = useChatMessage()
115103
const annotations = message.annotations as MessageAnnotation[] | undefined
116104

117105
const contents = useMemo<ContentDisplayConfig[]>(() => {
118106
const displayMap: {
119107
[key in ContentPosition]?: React.ReactNode | null
120108
} = {
121109
[ContentPosition.CHAT_EVENTS]: (
122-
<EventAnnotations message={message} showLoading={(isLast && props.isLoading) ?? false} />
110+
<EventAnnotations message={message} showLoading={(isLast && isLoading) ?? false} />
123111
),
124112
[ContentPosition.CHAT_AGENT_EVENTS]: <AgentEventAnnotations message={message} />,
125113
[ContentPosition.CHAT_IMAGE]: <ImageAnnotations message={message} />,
@@ -132,78 +120,52 @@ function ChatMessageContent(props: ChatMessageContentProps) {
132120
[ContentPosition.CHAT_DOCUMENT_FILES]: <DocumentFileAnnotations message={message} />,
133121
[ContentPosition.CHAT_SOURCES]: <SourceAnnotations message={message} />,
134122
...(isLast &&
135-
props.append && {
136-
// show suggested questions only on the last message
123+
append && {
124+
// Show suggested questions only on the last message.
137125
[ContentPosition.SUGGESTED_QUESTIONS]: (
138-
<SuggestedQuestionsAnnotations message={message} append={props.append} />
126+
<SuggestedQuestionsAnnotations message={message} append={append} />
139127
),
140128
}),
141129
}
142130

143-
// Override the default display map with the custom content
144-
props.content?.forEach((content) => {
131+
// Override the default display map with the custom content.
132+
content?.forEach((content) => {
145133
displayMap[content.position] = content.component
146134
})
147135

148136
return Object.entries(displayMap).map(([position, component]) => ({
149137
position: parseInt(position),
150138
component,
151139
}))
152-
}, [annotations, isLast, message, props.append, props.content, props.isLoading])
140+
}, [annotations, isLast, isLoading, content, append, message])
153141

154-
const children = props.children ?? (
155-
<>
142+
return (
143+
<div
144+
className={cn("flex flex-col gap-4 flex-1 min-w-0 px-2 pt-4 pb-6", {
145+
"text-right": message.role === "user",
146+
})}>
156147
{contents
157148
.sort((a, b) => a.position - b.position)
158149
.map((content, index) => (
159150
<Fragment key={index}>{content.component}</Fragment>
160151
))}
161-
</>
152+
</div>
162153
)
163-
164-
return <div className={cn("flex min-w-0 flex-1 flex-col gap-4", props.className)}>{children}</div>
165154
}
166155

167156
/**
168157
* ChatMessageActions
169158
*/
170159

171-
interface ChatMessageActionsProps extends React.PropsWithChildren {
172-
className?: string
173-
}
174-
175-
function ChatMessageActions(props: ChatMessageActionsProps) {
176-
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
160+
function ChatMessageActions() {
177161
const { message } = useChatMessage()
162+
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
178163

179-
const children = props.children ?? (
180-
<Button onClick={() => copyToClipboard(message.content)} size="icon" variant="ghost" className="h-8 w-8">
181-
{isCopied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
182-
</Button>
183-
)
184164
return (
185-
<div className={cn("flex shrink-0 flex-col gap-2 opacity-0 group-hover:opacity-100", props.className)}>
186-
{children}
165+
<div
166+
className="absolute right-2 bottom-2 opacity-0 group-hover:opacity-25 cursor-pointer"
167+
onClick={() => copyToClipboard(message.content)}>
168+
{isCopied ? <CheckIcon /> : <CopyIcon />}
187169
</div>
188170
)
189171
}
190-
191-
/**
192-
* ComposibleChatMessage
193-
*/
194-
195-
type ComposibleChatMessage = typeof ChatMessage & {
196-
Avatar: typeof ChatMessageAvatar
197-
Content: typeof ChatMessageContent
198-
Actions: typeof ChatMessageActions
199-
}
200-
201-
const PrimiviteChatMessage = memo(ChatMessage, (prevProps, nextProps) => {
202-
return !nextProps.isLast && prevProps.isLast === nextProps.isLast && prevProps.message === nextProps.message
203-
}) as unknown as ComposibleChatMessage
204-
205-
PrimiviteChatMessage.Avatar = ChatMessageAvatar
206-
PrimiviteChatMessage.Content = ChatMessageContent
207-
PrimiviteChatMessage.Actions = ChatMessageActions
208-
209-
export default PrimiviteChatMessage

0 commit comments

Comments
 (0)