|
@@ -25,6 +25,8 @@ import Thumbnails from "../common/Thumbnails"
|
|
|
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
|
|
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
|
|
|
import ContextMenu from "./ContextMenu"
|
|
import ContextMenu from "./ContextMenu"
|
|
|
import { VolumeX } from "lucide-react"
|
|
import { VolumeX } from "lucide-react"
|
|
|
|
|
+import { IconButton } from "./IconButton"
|
|
|
|
|
+import { cn } from "@/lib/utils"
|
|
|
|
|
|
|
|
interface ChatTextAreaProps {
|
|
interface ChatTextAreaProps {
|
|
|
inputValue: string
|
|
inputValue: string
|
|
@@ -113,7 +115,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
return () => window.removeEventListener("message", messageHandler)
|
|
return () => window.removeEventListener("message", messageHandler)
|
|
|
}, [setInputValue, searchRequestId])
|
|
}, [setInputValue, searchRequestId])
|
|
|
|
|
|
|
|
- const [thumbnailsHeight, setThumbnailsHeight] = useState(0)
|
|
|
|
|
|
|
+ const [isDraggingOver, setIsDraggingOver] = useState(false)
|
|
|
const [textAreaBaseHeight, setTextAreaBaseHeight] = useState<number | undefined>(undefined)
|
|
const [textAreaBaseHeight, setTextAreaBaseHeight] = useState<number | undefined>(undefined)
|
|
|
const [showContextMenu, setShowContextMenu] = useState(false)
|
|
const [showContextMenu, setShowContextMenu] = useState(false)
|
|
|
const [cursorPosition, setCursorPosition] = useState(0)
|
|
const [cursorPosition, setCursorPosition] = useState(0)
|
|
@@ -547,14 +549,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
[shouldDisableImages, setSelectedImages, cursorPosition, setInputValue, inputValue, t],
|
|
[shouldDisableImages, setSelectedImages, cursorPosition, setInputValue, inputValue, t],
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- const handleThumbnailsHeightChange = useCallback((height: number) => setThumbnailsHeight(height), [])
|
|
|
|
|
-
|
|
|
|
|
- useEffect(() => {
|
|
|
|
|
- if (selectedImages.length === 0) {
|
|
|
|
|
- setThumbnailsHeight(0)
|
|
|
|
|
- }
|
|
|
|
|
- }, [selectedImages])
|
|
|
|
|
-
|
|
|
|
|
const handleMenuMouseDown = useCallback(() => {
|
|
const handleMenuMouseDown = useCallback(() => {
|
|
|
setIsMouseDownOnMenu(true)
|
|
setIsMouseDownOnMenu(true)
|
|
|
}, [])
|
|
}, [])
|
|
@@ -592,75 +586,51 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
[updateCursorPosition],
|
|
[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"
|
|
|
|
|
- style={{
|
|
|
|
|
- opacity: textAreaDisabled ? 0.5 : 1,
|
|
|
|
|
- position: "relative",
|
|
|
|
|
- display: "flex",
|
|
|
|
|
- flexDirection: "column",
|
|
|
|
|
- gap: "8px",
|
|
|
|
|
- backgroundColor: "var(--vscode-input-background)",
|
|
|
|
|
- margin: "10px 15px",
|
|
|
|
|
- padding: "8px",
|
|
|
|
|
- outline: "none",
|
|
|
|
|
- border: "1px solid",
|
|
|
|
|
- borderColor: isFocused ? "var(--vscode-focusBorder)" : "transparent",
|
|
|
|
|
- borderRadius: "2px",
|
|
|
|
|
- }}
|
|
|
|
|
- onDrop={async (e) => {
|
|
|
|
|
- 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() !== "")
|
|
|
|
|
-
|
|
|
|
|
- if (lines.length > 0) {
|
|
|
|
|
- // Process each line as a separate file path
|
|
|
|
|
- let newValue = inputValue.slice(0, cursorPosition)
|
|
|
|
|
- let totalLength = 0
|
|
|
|
|
-
|
|
|
|
|
- lines.forEach((line, index) => {
|
|
|
|
|
- // Convert each path to a mention-friendly format
|
|
|
|
|
- const mentionText = convertToMentionPath(line, cwd)
|
|
|
|
|
- newValue += mentionText
|
|
|
|
|
- totalLength += mentionText.length
|
|
|
|
|
-
|
|
|
|
|
- // Add space after each mention except the last one
|
|
|
|
|
- if (index < lines.length - 1) {
|
|
|
|
|
- newValue += " "
|
|
|
|
|
- totalLength += 1
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- // Add space after the last mention and append the rest of the input
|
|
|
|
|
- newValue += " " + inputValue.slice(cursorPosition)
|
|
|
|
|
- totalLength += 1
|
|
|
|
|
-
|
|
|
|
|
- setInputValue(newValue)
|
|
|
|
|
- const newCursorPosition = cursorPosition + totalLength
|
|
|
|
|
- setCursorPosition(newCursorPosition)
|
|
|
|
|
- setIntendedCursorPosition(newCursorPosition)
|
|
|
|
|
|
|
+ const handleDrop = useCallback(
|
|
|
|
|
+ async (e: React.DragEvent<HTMLDivElement>) => {
|
|
|
|
|
+ e.preventDefault()
|
|
|
|
|
+ setIsDraggingOver(false)
|
|
|
|
|
+
|
|
|
|
|
+ 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() !== "")
|
|
|
|
|
+
|
|
|
|
|
+ if (lines.length > 0) {
|
|
|
|
|
+ // Process each line as a separate file path
|
|
|
|
|
+ let newValue = inputValue.slice(0, cursorPosition)
|
|
|
|
|
+ let totalLength = 0
|
|
|
|
|
+
|
|
|
|
|
+ // Using a standard for loop instead of forEach for potential performance gains.
|
|
|
|
|
+ for (let i = 0; i < lines.length; i++) {
|
|
|
|
|
+ const line = lines[i]
|
|
|
|
|
+ // Convert each path to a mention-friendly format
|
|
|
|
|
+ const mentionText = convertToMentionPath(line, cwd)
|
|
|
|
|
+ newValue += mentionText
|
|
|
|
|
+ totalLength += mentionText.length
|
|
|
|
|
+
|
|
|
|
|
+ // Add space after each mention except the last one
|
|
|
|
|
+ if (i < lines.length - 1) {
|
|
|
|
|
+ newValue += " "
|
|
|
|
|
+ totalLength += 1
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return
|
|
|
|
|
|
|
+ // Add space after the last mention and append the rest of the input
|
|
|
|
|
+ newValue += " " + inputValue.slice(cursorPosition)
|
|
|
|
|
+ totalLength += 1
|
|
|
|
|
+
|
|
|
|
|
+ setInputValue(newValue)
|
|
|
|
|
+ const newCursorPosition = cursorPosition + totalLength
|
|
|
|
|
+ setCursorPosition(newCursorPosition)
|
|
|
|
|
+ setIntendedCursorPosition(newCursorPosition)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const files = Array.from(e.dataTransfer.files)
|
|
|
|
|
+ if (!textAreaDisabled && files.length > 0) {
|
|
|
const acceptedTypes = ["png", "jpeg", "webp"]
|
|
const acceptedTypes = ["png", "jpeg", "webp"]
|
|
|
const imageFiles = files.filter((file) => {
|
|
const imageFiles = files.filter((file) => {
|
|
|
const [type, subtype] = file.type.split("/")
|
|
const [type, subtype] = file.type.split("/")
|
|
@@ -699,159 +669,256 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
console.warn(t("chat:noValidImages"))
|
|
console.warn(t("chat:noValidImages"))
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- }}
|
|
|
|
|
- onDragOver={(e) => {
|
|
|
|
|
- e.preventDefault()
|
|
|
|
|
- }}>
|
|
|
|
|
- {showContextMenu && (
|
|
|
|
|
- <div ref={contextMenuContainerRef}>
|
|
|
|
|
- <ContextMenu
|
|
|
|
|
- onSelect={handleMentionSelect}
|
|
|
|
|
- searchQuery={searchQuery}
|
|
|
|
|
- onMouseDown={handleMenuMouseDown}
|
|
|
|
|
- selectedIndex={selectedMenuIndex}
|
|
|
|
|
- setSelectedIndex={setSelectedMenuIndex}
|
|
|
|
|
- selectedType={selectedType}
|
|
|
|
|
- queryItems={queryItems}
|
|
|
|
|
- modes={getAllModes(customModes)}
|
|
|
|
|
- loading={searchLoading}
|
|
|
|
|
- dynamicSearchResults={fileSearchResults}
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ [
|
|
|
|
|
+ cursorPosition,
|
|
|
|
|
+ cwd,
|
|
|
|
|
+ inputValue,
|
|
|
|
|
+ setInputValue,
|
|
|
|
|
+ setCursorPosition,
|
|
|
|
|
+ setIntendedCursorPosition,
|
|
|
|
|
+ textAreaDisabled,
|
|
|
|
|
+ shouldDisableImages,
|
|
|
|
|
+ setSelectedImages,
|
|
|
|
|
+ t,
|
|
|
|
|
+ ],
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
- <div
|
|
|
|
|
- style={{
|
|
|
|
|
- position: "relative",
|
|
|
|
|
- flex: "1 1 auto",
|
|
|
|
|
- display: "flex",
|
|
|
|
|
- flexDirection: "column-reverse",
|
|
|
|
|
- minHeight: 0,
|
|
|
|
|
- overflow: "hidden",
|
|
|
|
|
- }}>
|
|
|
|
|
|
|
+ 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)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const placeholderBottomText = `\n(${t("chat:addContext")}${shouldDisableImages ? `, ${t("chat:dragFiles")}` : `, ${t("chat:dragFilesImages")}`})`
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div
|
|
|
|
|
+ className={cn(
|
|
|
|
|
+ "relative",
|
|
|
|
|
+ "flex",
|
|
|
|
|
+ "flex-col",
|
|
|
|
|
+ "gap-2",
|
|
|
|
|
+ "bg-editor-background",
|
|
|
|
|
+ "m-2 mt-1",
|
|
|
|
|
+ "p-1.5",
|
|
|
|
|
+ "outline-none",
|
|
|
|
|
+ "border",
|
|
|
|
|
+ "border-none",
|
|
|
|
|
+ "w-[calc(100%-16px)]",
|
|
|
|
|
+ "ml-auto",
|
|
|
|
|
+ "mr-auto",
|
|
|
|
|
+ "box-border",
|
|
|
|
|
+ )}>
|
|
|
|
|
+ <div className="relative">
|
|
|
<div
|
|
<div
|
|
|
- ref={highlightLayerRef}
|
|
|
|
|
- style={{
|
|
|
|
|
- position: "absolute",
|
|
|
|
|
- inset: 0,
|
|
|
|
|
- pointerEvents: "none",
|
|
|
|
|
- whiteSpace: "pre-wrap",
|
|
|
|
|
- wordWrap: "break-word",
|
|
|
|
|
- color: "transparent",
|
|
|
|
|
- overflow: "hidden",
|
|
|
|
|
- fontFamily: "var(--vscode-font-family)",
|
|
|
|
|
- fontSize: "var(--vscode-editor-font-size)",
|
|
|
|
|
- lineHeight: "var(--vscode-editor-line-height)",
|
|
|
|
|
- padding: "2px",
|
|
|
|
|
- paddingRight: "8px",
|
|
|
|
|
- marginBottom: thumbnailsHeight > 0 ? `${thumbnailsHeight + 16}px` : 0,
|
|
|
|
|
- zIndex: 1,
|
|
|
|
|
- }}
|
|
|
|
|
- />
|
|
|
|
|
- <DynamicTextArea
|
|
|
|
|
- ref={(el) => {
|
|
|
|
|
- if (typeof ref === "function") {
|
|
|
|
|
- ref(el)
|
|
|
|
|
- } else if (ref) {
|
|
|
|
|
- ref.current = el
|
|
|
|
|
|
|
+ className={cn("chat-text-area", "relative", "flex", "flex-col", "outline-none")}
|
|
|
|
|
+ onDrop={handleDrop}
|
|
|
|
|
+ onDragOver={(e) => {
|
|
|
|
|
+ //Only allowed to drop images/files on shift key pressed
|
|
|
|
|
+ if (!e.shiftKey) {
|
|
|
|
|
+ setIsDraggingOver(false)
|
|
|
|
|
+ return
|
|
|
}
|
|
}
|
|
|
- textAreaRef.current = el
|
|
|
|
|
- }}
|
|
|
|
|
- value={inputValue}
|
|
|
|
|
- disabled={textAreaDisabled}
|
|
|
|
|
- onChange={(e) => {
|
|
|
|
|
- handleInputChange(e)
|
|
|
|
|
- updateHighlights()
|
|
|
|
|
|
|
+ e.preventDefault()
|
|
|
|
|
+ setIsDraggingOver(true)
|
|
|
|
|
+ e.dataTransfer.dropEffect = "copy"
|
|
|
}}
|
|
}}
|
|
|
- onFocus={() => setIsFocused(true)}
|
|
|
|
|
- onKeyDown={handleKeyDown}
|
|
|
|
|
- onKeyUp={handleKeyUp}
|
|
|
|
|
- onBlur={handleBlur}
|
|
|
|
|
- onPaste={handlePaste}
|
|
|
|
|
- onSelect={updateCursorPosition}
|
|
|
|
|
- onMouseUp={updateCursorPosition}
|
|
|
|
|
- onHeightChange={(height) => {
|
|
|
|
|
- if (textAreaBaseHeight === undefined || height < textAreaBaseHeight) {
|
|
|
|
|
- setTextAreaBaseHeight(height)
|
|
|
|
|
|
|
+ onDragLeave={(e) => {
|
|
|
|
|
+ e.preventDefault()
|
|
|
|
|
+ const rect = e.currentTarget.getBoundingClientRect()
|
|
|
|
|
+ if (
|
|
|
|
|
+ e.clientX <= rect.left ||
|
|
|
|
|
+ e.clientX >= rect.right ||
|
|
|
|
|
+ e.clientY <= rect.top ||
|
|
|
|
|
+ e.clientY >= rect.bottom
|
|
|
|
|
+ ) {
|
|
|
|
|
+ setIsDraggingOver(false)
|
|
|
}
|
|
}
|
|
|
- onHeightChange?.(height)
|
|
|
|
|
- }}
|
|
|
|
|
- placeholder={placeholderText}
|
|
|
|
|
- minRows={3}
|
|
|
|
|
- maxRows={15}
|
|
|
|
|
- autoFocus={true}
|
|
|
|
|
- style={{
|
|
|
|
|
- width: "100%",
|
|
|
|
|
- outline: "none",
|
|
|
|
|
- boxSizing: "border-box",
|
|
|
|
|
- backgroundColor: "transparent",
|
|
|
|
|
- color: "var(--vscode-input-foreground)",
|
|
|
|
|
- borderRadius: 2,
|
|
|
|
|
- fontFamily: "var(--vscode-font-family)",
|
|
|
|
|
- fontSize: "var(--vscode-editor-font-size)",
|
|
|
|
|
- lineHeight: "var(--vscode-editor-line-height)",
|
|
|
|
|
- resize: "none",
|
|
|
|
|
- overflowX: "hidden",
|
|
|
|
|
- overflowY: "auto",
|
|
|
|
|
- border: "none",
|
|
|
|
|
- padding: "2px",
|
|
|
|
|
- paddingRight: "8px",
|
|
|
|
|
- marginBottom: thumbnailsHeight > 0 ? `${thumbnailsHeight + 16}px` : 0,
|
|
|
|
|
- cursor: textAreaDisabled ? "not-allowed" : undefined,
|
|
|
|
|
- flex: "0 1 auto",
|
|
|
|
|
- zIndex: 2,
|
|
|
|
|
- scrollbarWidth: "none",
|
|
|
|
|
- }}
|
|
|
|
|
- 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>
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ }}>
|
|
|
|
|
+ {showContextMenu && (
|
|
|
|
|
+ <div
|
|
|
|
|
+ ref={contextMenuContainerRef}
|
|
|
|
|
+ className={cn(
|
|
|
|
|
+ "absolute",
|
|
|
|
|
+ "bottom-full",
|
|
|
|
|
+ "left-0",
|
|
|
|
|
+ "right-0",
|
|
|
|
|
+ "z-[1000]",
|
|
|
|
|
+ "mb-2",
|
|
|
|
|
+ "filter",
|
|
|
|
|
+ "drop-shadow-md",
|
|
|
|
|
+ )}>
|
|
|
|
|
+ <ContextMenu
|
|
|
|
|
+ onSelect={handleMentionSelect}
|
|
|
|
|
+ searchQuery={searchQuery}
|
|
|
|
|
+ onMouseDown={handleMenuMouseDown}
|
|
|
|
|
+ selectedIndex={selectedMenuIndex}
|
|
|
|
|
+ setSelectedIndex={setSelectedMenuIndex}
|
|
|
|
|
+ selectedType={selectedType}
|
|
|
|
|
+ queryItems={queryItems}
|
|
|
|
|
+ modes={getAllModes(customModes)}
|
|
|
|
|
+ loading={searchLoading}
|
|
|
|
|
+ dynamicSearchResults={fileSearchResults}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ <div
|
|
|
|
|
+ className={cn(
|
|
|
|
|
+ "relative",
|
|
|
|
|
+ "flex-1",
|
|
|
|
|
+ "flex",
|
|
|
|
|
+ "flex-col-reverse",
|
|
|
|
|
+ "min-h-0",
|
|
|
|
|
+ "overflow-hidden",
|
|
|
|
|
+ "rounded",
|
|
|
|
|
+ )}>
|
|
|
|
|
+ <div
|
|
|
|
|
+ ref={highlightLayerRef}
|
|
|
|
|
+ className={cn(
|
|
|
|
|
+ "absolute",
|
|
|
|
|
+ "inset-0",
|
|
|
|
|
+ "pointer-events-none",
|
|
|
|
|
+ "whitespace-pre-wrap",
|
|
|
|
|
+ "break-words",
|
|
|
|
|
+ "text-transparent",
|
|
|
|
|
+ "overflow-hidden",
|
|
|
|
|
+ "font-vscode-font-family",
|
|
|
|
|
+ "text-vscode-editor-font-size",
|
|
|
|
|
+ "leading-vscode-editor-line-height",
|
|
|
|
|
+ "py-2",
|
|
|
|
|
+ "px-[9px]",
|
|
|
|
|
+ "z-[1000]",
|
|
|
|
|
+ )}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ color: "transparent",
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ <DynamicTextArea
|
|
|
|
|
+ ref={(el) => {
|
|
|
|
|
+ if (typeof ref === "function") {
|
|
|
|
|
+ ref(el)
|
|
|
|
|
+ } else if (ref) {
|
|
|
|
|
+ ref.current = el
|
|
|
|
|
+ }
|
|
|
|
|
+ textAreaRef.current = el
|
|
|
|
|
+ }}
|
|
|
|
|
+ value={inputValue}
|
|
|
|
|
+ disabled={textAreaDisabled}
|
|
|
|
|
+ onChange={(e) => {
|
|
|
|
|
+ handleInputChange(e)
|
|
|
|
|
+ updateHighlights()
|
|
|
|
|
+ }}
|
|
|
|
|
+ onFocus={() => setIsFocused(true)}
|
|
|
|
|
+ onKeyDown={handleKeyDown}
|
|
|
|
|
+ onKeyUp={handleKeyUp}
|
|
|
|
|
+ onBlur={handleBlur}
|
|
|
|
|
+ onPaste={handlePaste}
|
|
|
|
|
+ onSelect={updateCursorPosition}
|
|
|
|
|
+ onMouseUp={updateCursorPosition}
|
|
|
|
|
+ onHeightChange={(height) => {
|
|
|
|
|
+ if (textAreaBaseHeight === undefined || height < textAreaBaseHeight) {
|
|
|
|
|
+ setTextAreaBaseHeight(height)
|
|
|
|
|
+ }
|
|
|
|
|
+ onHeightChange?.(height)
|
|
|
|
|
+ }}
|
|
|
|
|
+ placeholder={placeholderText}
|
|
|
|
|
+ minRows={3}
|
|
|
|
|
+ maxRows={15}
|
|
|
|
|
+ autoFocus={true}
|
|
|
|
|
+ className={cn(
|
|
|
|
|
+ "w-full",
|
|
|
|
|
+ "text-vscode-input-foreground",
|
|
|
|
|
+ "font-vscode-font-family",
|
|
|
|
|
+ "text-vscode-editor-font-size",
|
|
|
|
|
+ "leading-vscode-editor-line-height",
|
|
|
|
|
+ textAreaDisabled ? "cursor-not-allowed" : "cursor-text",
|
|
|
|
|
+ "py-1.5 px-2",
|
|
|
|
|
+ isFocused
|
|
|
|
|
+ ? "border border-vscode-focusBorder outline outline-vscode-focusBorder"
|
|
|
|
|
+ : isDraggingOver
|
|
|
|
|
+ ? "border-2 border-dashed border-vscode-focusBorder"
|
|
|
|
|
+ : "border border-transparent",
|
|
|
|
|
+ textAreaDisabled ? "opacity-50" : "opacity-100",
|
|
|
|
|
+ isDraggingOver
|
|
|
|
|
+ ? "bg-[color-mix(in_srgb,var(--vscode-input-background)_95%,var(--vscode-focusBorder))]"
|
|
|
|
|
+ : "bg-vscode-input-background",
|
|
|
|
|
+ "transition-background-color duration-150 ease-in-out",
|
|
|
|
|
+ "will-change-background-color",
|
|
|
|
|
+ "h-[100px]",
|
|
|
|
|
+ "[@media(min-width:150px)]:min-h-[80px]",
|
|
|
|
|
+ "[@media(min-width:425px)]:min-h-[60px]",
|
|
|
|
|
+ "box-border",
|
|
|
|
|
+ "rounded",
|
|
|
|
|
+ "resize-none",
|
|
|
|
|
+ "overflow-x-hidden",
|
|
|
|
|
+ "overflow-y-auto",
|
|
|
|
|
+ "pr-2",
|
|
|
|
|
+ "flex-none flex-grow",
|
|
|
|
|
+ "z-[2]",
|
|
|
|
|
+ "scrollbar-none",
|
|
|
|
|
+ )}
|
|
|
|
|
+ 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>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {!inputValue && (
|
|
|
|
|
+ <div
|
|
|
|
|
+ className={cn(
|
|
|
|
|
+ "absolute",
|
|
|
|
|
+ "left-2",
|
|
|
|
|
+ "flex",
|
|
|
|
|
+ "gap-2",
|
|
|
|
|
+ "text-xs",
|
|
|
|
|
+ "text-descriptionForeground",
|
|
|
|
|
+ "pointer-events-none",
|
|
|
|
|
+ "z-25",
|
|
|
|
|
+ "bottom-1.5",
|
|
|
|
|
+ "pr-2",
|
|
|
|
|
+ "transition-opacity",
|
|
|
|
|
+ "duration-200",
|
|
|
|
|
+ "ease-in-out",
|
|
|
|
|
+ textAreaDisabled ? "opacity-35" : "opacity-70",
|
|
|
|
|
+ )}>
|
|
|
|
|
+ {placeholderBottomText}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
{selectedImages.length > 0 && (
|
|
{selectedImages.length > 0 && (
|
|
|
<Thumbnails
|
|
<Thumbnails
|
|
|
images={selectedImages}
|
|
images={selectedImages}
|
|
|
setImages={setSelectedImages}
|
|
setImages={setSelectedImages}
|
|
|
- onHeightChange={handleThumbnailsHeightChange}
|
|
|
|
|
style={{
|
|
style={{
|
|
|
- position: "absolute",
|
|
|
|
|
- bottom: "36px",
|
|
|
|
|
left: "16px",
|
|
left: "16px",
|
|
|
zIndex: 2,
|
|
zIndex: 2,
|
|
|
- marginBottom: "4px",
|
|
|
|
|
|
|
+ marginBottom: 0,
|
|
|
}}
|
|
}}
|
|
|
/>
|
|
/>
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
- <div
|
|
|
|
|
- style={{
|
|
|
|
|
- display: "flex",
|
|
|
|
|
- justifyContent: "space-between",
|
|
|
|
|
- alignItems: "center",
|
|
|
|
|
- marginTop: "auto",
|
|
|
|
|
- paddingTop: "2px",
|
|
|
|
|
- }}>
|
|
|
|
|
- {/* Left side - dropdowns container */}
|
|
|
|
|
- <div
|
|
|
|
|
- style={{
|
|
|
|
|
- display: "flex",
|
|
|
|
|
- alignItems: "center",
|
|
|
|
|
- gap: "4px",
|
|
|
|
|
- overflow: "hidden",
|
|
|
|
|
- minWidth: 0,
|
|
|
|
|
- }}>
|
|
|
|
|
|
|
+ <div className={cn("flex", "justify-between", "items-center", "mt-auto", "pt-0.5")}>
|
|
|
|
|
+ <div className={cn("flex", "items-center", "gap-1", "min-w-0")}>
|
|
|
{/* Mode selector - fixed width */}
|
|
{/* Mode selector - fixed width */}
|
|
|
- <div style={{ flexShrink: 0 }}>
|
|
|
|
|
|
|
+ <div className="shrink-0">
|
|
|
<SelectDropdown
|
|
<SelectDropdown
|
|
|
value={mode}
|
|
value={mode}
|
|
|
- disabled={textAreaDisabled}
|
|
|
|
|
title={t("chat:selectMode")}
|
|
title={t("chat:selectMode")}
|
|
|
options={[
|
|
options={[
|
|
|
{
|
|
{
|
|
@@ -886,12 +953,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
{/* API configuration selector - flexible width */}
|
|
{/* API configuration selector - flexible width */}
|
|
|
- <div
|
|
|
|
|
- style={{
|
|
|
|
|
- flex: "1 1 auto",
|
|
|
|
|
- minWidth: 0,
|
|
|
|
|
- overflow: "hidden",
|
|
|
|
|
- }}>
|
|
|
|
|
|
|
+ <div className={cn("flex-1", "min-w-0", "overflow-hidden")}>
|
|
|
<SelectDropdown
|
|
<SelectDropdown
|
|
|
value={currentApiConfigName || ""}
|
|
value={currentApiConfigName || ""}
|
|
|
disabled={textAreaDisabled}
|
|
disabled={textAreaDisabled}
|
|
@@ -921,51 +983,25 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
{/* Right side - action buttons */}
|
|
{/* Right side - action buttons */}
|
|
|
- <div
|
|
|
|
|
- style={{
|
|
|
|
|
- display: "flex",
|
|
|
|
|
- alignItems: "center",
|
|
|
|
|
- gap: "8px",
|
|
|
|
|
- flexShrink: 0,
|
|
|
|
|
- }}>
|
|
|
|
|
- <div style={{ display: "flex", alignItems: "center" }}>
|
|
|
|
|
- {isEnhancingPrompt ? (
|
|
|
|
|
- <span
|
|
|
|
|
- className="codicon codicon-loading codicon-modifier-spin"
|
|
|
|
|
- style={{
|
|
|
|
|
- color: "var(--vscode-input-foreground)",
|
|
|
|
|
- opacity: 0.5,
|
|
|
|
|
- fontSize: 16.5,
|
|
|
|
|
- marginRight: 6,
|
|
|
|
|
- }}
|
|
|
|
|
- />
|
|
|
|
|
- ) : (
|
|
|
|
|
- <span
|
|
|
|
|
- role="button"
|
|
|
|
|
- aria-label="enhance prompt"
|
|
|
|
|
- data-testid="enhance-prompt-button"
|
|
|
|
|
- title={t("chat:enhancePrompt")}
|
|
|
|
|
- className={`input-icon-button ${
|
|
|
|
|
- textAreaDisabled ? "disabled" : ""
|
|
|
|
|
- } codicon codicon-sparkle`}
|
|
|
|
|
- onClick={() => !textAreaDisabled && handleEnhancePrompt()}
|
|
|
|
|
- style={{ fontSize: 16.5 }}
|
|
|
|
|
- />
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
- <span
|
|
|
|
|
- className={`input-icon-button ${
|
|
|
|
|
- shouldDisableImages ? "disabled" : ""
|
|
|
|
|
- } codicon codicon-device-camera`}
|
|
|
|
|
|
|
+ <div className={cn("flex", "items-center", "gap-0.5", "shrink-0")}>
|
|
|
|
|
+ <IconButton
|
|
|
|
|
+ iconClass={isEnhancingPrompt ? "codicon-loading" : "codicon-sparkle"}
|
|
|
|
|
+ title={t("chat:enhancePrompt")}
|
|
|
|
|
+ disabled={textAreaDisabled}
|
|
|
|
|
+ isLoading={isEnhancingPrompt}
|
|
|
|
|
+ onClick={handleEnhancePrompt}
|
|
|
|
|
+ />
|
|
|
|
|
+ <IconButton
|
|
|
|
|
+ iconClass="codicon-device-camera"
|
|
|
title={t("chat:addImages")}
|
|
title={t("chat:addImages")}
|
|
|
- onClick={() => !shouldDisableImages && onSelectImages()}
|
|
|
|
|
- style={{ fontSize: 16.5 }}
|
|
|
|
|
|
|
+ disabled={shouldDisableImages}
|
|
|
|
|
+ onClick={onSelectImages}
|
|
|
/>
|
|
/>
|
|
|
- <span
|
|
|
|
|
- className={`input-icon-button ${textAreaDisabled ? "disabled" : ""} codicon codicon-send`}
|
|
|
|
|
|
|
+ <IconButton
|
|
|
|
|
+ iconClass="codicon-send"
|
|
|
title={t("chat:sendMessage")}
|
|
title={t("chat:sendMessage")}
|
|
|
- onClick={() => !textAreaDisabled && onSend()}
|
|
|
|
|
- style={{ fontSize: 15 }}
|
|
|
|
|
|
|
+ disabled={textAreaDisabled}
|
|
|
|
|
+ onClick={onSend}
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|