|
|
@@ -1,11 +1,15 @@
|
|
|
import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
|
|
|
+import { useEvent } from "react-use"
|
|
|
import DynamicTextArea from "react-textarea-autosize"
|
|
|
|
|
|
import { mentionRegex, mentionRegexGlobal } from "../../../../src/shared/context-mentions"
|
|
|
import { WebviewMessage } from "../../../../src/shared/WebviewMessage"
|
|
|
import { Mode, getAllModes } from "../../../../src/shared/modes"
|
|
|
+import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
|
|
|
|
|
|
import { vscode } from "@/utils/vscode"
|
|
|
+import { useExtensionState } from "@/context/ExtensionStateContext"
|
|
|
+import { useAppTranslation } from "@/i18n/TranslationContext"
|
|
|
import {
|
|
|
ContextMenuOptionType,
|
|
|
getContextMenuOptions,
|
|
|
@@ -13,14 +17,13 @@ import {
|
|
|
removeMention,
|
|
|
shouldShowContextMenu,
|
|
|
} from "@/utils/context-mentions"
|
|
|
-import { SelectDropdown, DropdownOptionType } from "@/components/ui"
|
|
|
+import { convertToMentionPath } from "@/utils/path-mentions"
|
|
|
+import { SelectDropdown, DropdownOptionType, Button } from "@/components/ui"
|
|
|
|
|
|
-import { useExtensionState } from "../../context/ExtensionStateContext"
|
|
|
import Thumbnails from "../common/Thumbnails"
|
|
|
-import { convertToMentionPath } from "../../utils/path-mentions"
|
|
|
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
|
|
|
import ContextMenu from "./ContextMenu"
|
|
|
-import { useAppTranslation } from "../../i18n/TranslationContext"
|
|
|
+import { VolumeX } from "lucide-react"
|
|
|
|
|
|
interface ChatTextAreaProps {
|
|
|
inputValue: string
|
|
|
@@ -62,7 +65,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
const [gitCommits, setGitCommits] = useState<any[]>([])
|
|
|
const [showDropdown, setShowDropdown] = useState(false)
|
|
|
|
|
|
- // Close dropdown when clicking outside
|
|
|
+ // Close dropdown when clicking outside.
|
|
|
useEffect(() => {
|
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
|
if (showDropdown) {
|
|
|
@@ -73,14 +76,16 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
return () => document.removeEventListener("mousedown", handleClickOutside)
|
|
|
}, [showDropdown])
|
|
|
|
|
|
- // Handle enhanced prompt response
|
|
|
+ // Handle enhanced prompt response.
|
|
|
useEffect(() => {
|
|
|
const messageHandler = (event: MessageEvent) => {
|
|
|
const message = event.data
|
|
|
+
|
|
|
if (message.type === "enhancedPrompt") {
|
|
|
if (message.text) {
|
|
|
setInputValue(message.text)
|
|
|
}
|
|
|
+
|
|
|
setIsEnhancingPrompt(false)
|
|
|
} else if (message.type === "commitSearchResults") {
|
|
|
const commits = message.commits.map((commit: any) => ({
|
|
|
@@ -90,9 +95,11 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
description: `${commit.shortHash} by ${commit.author} on ${commit.date}`,
|
|
|
icon: "$(git-commit)",
|
|
|
}))
|
|
|
+
|
|
|
setGitCommits(commits)
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
window.addEventListener("message", messageHandler)
|
|
|
return () => window.removeEventListener("message", messageHandler)
|
|
|
}, [setInputValue])
|
|
|
@@ -113,7 +120,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
const [isEnhancingPrompt, setIsEnhancingPrompt] = useState(false)
|
|
|
const [isFocused, setIsFocused] = useState(false)
|
|
|
|
|
|
- // Fetch git commits when Git is selected or when typing a hash
|
|
|
+ // Fetch git commits when Git is selected or when typing a hash.
|
|
|
useEffect(() => {
|
|
|
if (selectedType === ContextMenuOptionType.Git || /^[a-f0-9]+$/i.test(searchQuery)) {
|
|
|
const message: WebviewMessage = {
|
|
|
@@ -188,14 +195,11 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
}
|
|
|
|
|
|
if (type === ContextMenuOptionType.Mode && value) {
|
|
|
- // Handle mode selection
|
|
|
+ // Handle mode selection.
|
|
|
setMode(value)
|
|
|
setInputValue("")
|
|
|
setShowContextMenu(false)
|
|
|
- vscode.postMessage({
|
|
|
- type: "mode",
|
|
|
- text: value,
|
|
|
- })
|
|
|
+ vscode.postMessage({ type: "mode", text: value })
|
|
|
return
|
|
|
}
|
|
|
|
|
|
@@ -214,8 +218,10 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
|
|
|
setShowContextMenu(false)
|
|
|
setSelectedType(null)
|
|
|
+
|
|
|
if (textAreaRef.current) {
|
|
|
let insertValue = value || ""
|
|
|
+
|
|
|
if (type === ContextMenuOptionType.URL) {
|
|
|
insertValue = value || ""
|
|
|
} else if (type === ContextMenuOptionType.File || type === ContextMenuOptionType.Folder) {
|
|
|
@@ -239,7 +245,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
setCursorPosition(newCursorPosition)
|
|
|
setIntendedCursorPosition(newCursorPosition)
|
|
|
|
|
|
- // scroll to cursor
|
|
|
+ // Scroll to cursor.
|
|
|
setTimeout(() => {
|
|
|
if (textAreaRef.current) {
|
|
|
textAreaRef.current.blur()
|
|
|
@@ -378,7 +384,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
useLayoutEffect(() => {
|
|
|
if (intendedCursorPosition !== null && textAreaRef.current) {
|
|
|
textAreaRef.current.setSelectionRange(intendedCursorPosition, intendedCursorPosition)
|
|
|
- setIntendedCursorPosition(null) // Reset the state
|
|
|
+ setIntendedCursorPosition(null) // Reset the state.
|
|
|
}
|
|
|
}, [inputValue, intendedCursorPosition])
|
|
|
|
|
|
@@ -423,10 +429,11 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
}, [showContextMenu])
|
|
|
|
|
|
const handleBlur = useCallback(() => {
|
|
|
- // Only hide the context menu if the user didn't click on it
|
|
|
+ // Only hide the context menu if the user didn't click on it.
|
|
|
if (!isMouseDownOnMenu) {
|
|
|
setShowContextMenu(false)
|
|
|
}
|
|
|
+
|
|
|
setIsFocused(false)
|
|
|
}, [isMouseDownOnMenu])
|
|
|
|
|
|
@@ -435,7 +442,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
const items = e.clipboardData.items
|
|
|
|
|
|
const pastedText = e.clipboardData.getData("text")
|
|
|
- // Check if the pasted content is a URL, add space after so user can easily delete if they don't want it
|
|
|
+ // Check if the pasted content is a URL, add space after so user
|
|
|
+ // can easily delete if they don't want it.
|
|
|
const urlRegex = /^\S+:\/\/\S+$/
|
|
|
if (urlRegex.test(pastedText.trim())) {
|
|
|
e.preventDefault()
|
|
|
@@ -448,7 +456,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
setIntendedCursorPosition(newCursorPosition)
|
|
|
setShowContextMenu(false)
|
|
|
|
|
|
- // Scroll to new cursor position
|
|
|
+ // Scroll to new cursor position.
|
|
|
setTimeout(() => {
|
|
|
if (textAreaRef.current) {
|
|
|
textAreaRef.current.blur()
|
|
|
@@ -460,10 +468,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
}
|
|
|
|
|
|
const acceptedTypes = ["png", "jpeg", "webp"]
|
|
|
+
|
|
|
const imageItems = Array.from(items).filter((item) => {
|
|
|
const [type, subtype] = item.type.split("/")
|
|
|
return type === "image" && acceptedTypes.includes(subtype)
|
|
|
})
|
|
|
+
|
|
|
if (!shouldDisableImages && imageItems.length > 0) {
|
|
|
e.preventDefault()
|
|
|
const imagePromises = imageItems.map((item) => {
|
|
|
@@ -498,9 +508,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
[shouldDisableImages, setSelectedImages, cursorPosition, setInputValue, inputValue, t],
|
|
|
)
|
|
|
|
|
|
- const handleThumbnailsHeightChange = useCallback((height: number) => {
|
|
|
- setThumbnailsHeight(height)
|
|
|
- }, [])
|
|
|
+ const handleThumbnailsHeightChange = useCallback((height: number) => setThumbnailsHeight(height), [])
|
|
|
|
|
|
useEffect(() => {
|
|
|
if (selectedImages.length === 0) {
|
|
|
@@ -545,6 +553,18 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
[updateCursorPosition],
|
|
|
)
|
|
|
|
|
|
+ const [isTtsPlaying, setIsTtsPlaying] = useState(false)
|
|
|
+
|
|
|
+ useEvent("message", (event: MessageEvent) => {
|
|
|
+ const message: ExtensionMessage = event.data
|
|
|
+
|
|
|
+ if (message.type === "ttsStart") {
|
|
|
+ setIsTtsPlaying(true)
|
|
|
+ } else if (message.type === "ttsStop") {
|
|
|
+ setIsTtsPlaying(false)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
return (
|
|
|
<div
|
|
|
className="chat-text-area"
|
|
|
@@ -566,6 +586,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
e.preventDefault()
|
|
|
const files = Array.from(e.dataTransfer.files)
|
|
|
const text = e.dataTransfer.getData("text")
|
|
|
+
|
|
|
if (text) {
|
|
|
// Split text on newlines to handle multiple files
|
|
|
const lines = text.split(/\r?\n/).filter((line) => line.trim() !== "")
|
|
|
@@ -597,6 +618,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
setCursorPosition(newCursorPosition)
|
|
|
setIntendedCursorPosition(newCursorPosition)
|
|
|
}
|
|
|
+
|
|
|
return
|
|
|
}
|
|
|
|
|
|
@@ -741,6 +763,15 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
}}
|
|
|
onScroll={() => updateHighlights()}
|
|
|
/>
|
|
|
+ {isTtsPlaying && (
|
|
|
+ <Button
|
|
|
+ variant="ghost"
|
|
|
+ size="icon"
|
|
|
+ className="absolute top-0 right-0 opacity-25 hover:opacity-100 z-10"
|
|
|
+ onClick={() => vscode.postMessage({ type: "stopTts" })}>
|
|
|
+ <VolumeX className="size-4" />
|
|
|
+ </Button>
|
|
|
+ )}
|
|
|
</div>
|
|
|
|
|
|
{selectedImages.length > 0 && (
|
|
|
@@ -782,26 +813,22 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
disabled={textAreaDisabled}
|
|
|
title={t("chat:selectMode")}
|
|
|
options={[
|
|
|
- // Add the shortcut text as a disabled option at the top
|
|
|
{
|
|
|
value: "shortcut",
|
|
|
label: modeShortcutText,
|
|
|
disabled: true,
|
|
|
type: DropdownOptionType.SHORTCUT,
|
|
|
},
|
|
|
- // Add all modes
|
|
|
...getAllModes(customModes).map((mode) => ({
|
|
|
value: mode.slug,
|
|
|
label: mode.name,
|
|
|
type: DropdownOptionType.ITEM,
|
|
|
})),
|
|
|
- // Add separator
|
|
|
{
|
|
|
value: "sep-1",
|
|
|
label: t("chat:separator"),
|
|
|
type: DropdownOptionType.SEPARATOR,
|
|
|
},
|
|
|
- // Add Edit option
|
|
|
{
|
|
|
value: "promptsButtonClicked",
|
|
|
label: t("chat:edit"),
|
|
|
@@ -810,10 +837,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
]}
|
|
|
onChange={(value) => {
|
|
|
setMode(value as Mode)
|
|
|
- vscode.postMessage({
|
|
|
- type: "mode",
|
|
|
- text: value,
|
|
|
- })
|
|
|
+ vscode.postMessage({ type: "mode", text: value })
|
|
|
}}
|
|
|
shortcutText={modeShortcutText}
|
|
|
triggerClassName="w-full"
|
|
|
@@ -832,31 +856,23 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
disabled={textAreaDisabled}
|
|
|
title={t("chat:selectApiConfig")}
|
|
|
options={[
|
|
|
- // Add all API configurations
|
|
|
...(listApiConfigMeta || []).map((config) => ({
|
|
|
value: config.name,
|
|
|
label: config.name,
|
|
|
type: DropdownOptionType.ITEM,
|
|
|
})),
|
|
|
- // Add separator
|
|
|
{
|
|
|
value: "sep-2",
|
|
|
label: t("chat:separator"),
|
|
|
type: DropdownOptionType.SEPARATOR,
|
|
|
},
|
|
|
- // Add Edit option
|
|
|
{
|
|
|
value: "settingsButtonClicked",
|
|
|
label: t("chat:edit"),
|
|
|
type: DropdownOptionType.ACTION,
|
|
|
},
|
|
|
]}
|
|
|
- onChange={(value) => {
|
|
|
- vscode.postMessage({
|
|
|
- type: "loadApiConfiguration",
|
|
|
- text: value,
|
|
|
- })
|
|
|
- }}
|
|
|
+ onChange={(value) => vscode.postMessage({ type: "loadApiConfiguration", text: value })}
|
|
|
contentClassName="max-h-[300px] overflow-y-auto"
|
|
|
triggerClassName="w-full text-ellipsis overflow-hidden"
|
|
|
/>
|