680 lines
28 KiB
JavaScript
680 lines
28 KiB
JavaScript
import { CopilotChatDefaultLabels, useCopilotChatConfiguration } from "../../providers/CopilotChatConfigurationProvider.mjs";
|
|
import { cn } from "../../lib/utils.mjs";
|
|
import { Button } from "../ui/button.mjs";
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.mjs";
|
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from "../ui/dropdown-menu.mjs";
|
|
import { CopilotChatAudioRecorder } from "./CopilotChatAudioRecorder.mjs";
|
|
import { renderSlot } from "../../lib/slots.mjs";
|
|
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
import { twMerge } from "tailwind-merge";
|
|
import { ArrowUp, Check, Loader2, Mic, Plus, Square, X } from "lucide-react";
|
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
|
//#region src/components/chat/CopilotChatInput.tsx
|
|
const SLASH_MENU_MAX_VISIBLE_ITEMS = 5;
|
|
const SLASH_MENU_ITEM_HEIGHT_PX = 40;
|
|
function CopilotChatInput({ mode = "input", onSubmitMessage, onStop, isRunning = false, onStartTranscribe, onCancelTranscribe, onFinishTranscribe, onFinishTranscribeWithAudio, onAddFile, onChange, value, toolsMenu, autoFocus = true, positioning = "static", keyboardHeight = 0, containerRef, showDisclaimer, textArea, sendButton, startTranscribeButton, cancelTranscribeButton, finishTranscribeButton, addMenuButton, audioRecorder, disclaimer, children, className, ...props }) {
|
|
const isControlled = value !== void 0;
|
|
const [internalValue, setInternalValue] = useState(() => value ?? "");
|
|
useEffect(() => {
|
|
if (!isControlled && value !== void 0) setInternalValue(value);
|
|
}, [isControlled, value]);
|
|
const resolvedValue = isControlled ? value ?? "" : internalValue;
|
|
const [layout, setLayout] = useState("compact");
|
|
const ignoreResizeRef = useRef(false);
|
|
const resizeEvaluationRafRef = useRef(null);
|
|
const isExpanded = mode === "input" && layout === "expanded";
|
|
const [commandQuery, setCommandQuery] = useState(null);
|
|
const [slashHighlightIndex, setSlashHighlightIndex] = useState(0);
|
|
const inputRef = useRef(null);
|
|
const gridRef = useRef(null);
|
|
const addButtonContainerRef = useRef(null);
|
|
const actionsContainerRef = useRef(null);
|
|
const audioRecorderRef = useRef(null);
|
|
const slashMenuRef = useRef(null);
|
|
const config = useCopilotChatConfiguration();
|
|
const labels = config?.labels ?? CopilotChatDefaultLabels;
|
|
const previousModalStateRef = useRef(void 0);
|
|
const measurementCanvasRef = useRef(null);
|
|
const measurementsRef = useRef({
|
|
singleLineHeight: 0,
|
|
maxHeight: 0,
|
|
paddingLeft: 0,
|
|
paddingRight: 0
|
|
});
|
|
const commandItems = useMemo(() => {
|
|
const entries = [];
|
|
const seen = /* @__PURE__ */ new Set();
|
|
const pushItem = (item) => {
|
|
if (item === "-") return;
|
|
if (item.items && item.items.length > 0) {
|
|
for (const nested of item.items) pushItem(nested);
|
|
return;
|
|
}
|
|
if (!seen.has(item.label)) {
|
|
seen.add(item.label);
|
|
entries.push(item);
|
|
}
|
|
};
|
|
if (onAddFile) pushItem({
|
|
label: labels.chatInputToolbarAddButtonLabel,
|
|
action: onAddFile
|
|
});
|
|
if (toolsMenu && toolsMenu.length > 0) for (const item of toolsMenu) pushItem(item);
|
|
return entries;
|
|
}, [
|
|
labels.chatInputToolbarAddButtonLabel,
|
|
onAddFile,
|
|
toolsMenu
|
|
]);
|
|
const filteredCommands = useMemo(() => {
|
|
if (commandQuery === null) return [];
|
|
if (commandItems.length === 0) return [];
|
|
const query = commandQuery.trim().toLowerCase();
|
|
if (query.length === 0) return commandItems;
|
|
const startsWith = [];
|
|
const contains = [];
|
|
for (const item of commandItems) {
|
|
const label = item.label.toLowerCase();
|
|
if (label.startsWith(query)) startsWith.push(item);
|
|
else if (label.includes(query)) contains.push(item);
|
|
}
|
|
return [...startsWith, ...contains];
|
|
}, [commandItems, commandQuery]);
|
|
useEffect(() => {
|
|
if (!autoFocus) {
|
|
previousModalStateRef.current = config?.isModalOpen;
|
|
return;
|
|
}
|
|
if (config?.isModalOpen && !previousModalStateRef.current) inputRef.current?.focus();
|
|
previousModalStateRef.current = config?.isModalOpen;
|
|
}, [config?.isModalOpen, autoFocus]);
|
|
useEffect(() => {
|
|
if (commandItems.length === 0 && commandQuery !== null) setCommandQuery(null);
|
|
}, [commandItems.length, commandQuery]);
|
|
const previousCommandQueryRef = useRef(null);
|
|
useEffect(() => {
|
|
if (commandQuery !== null && commandQuery !== previousCommandQueryRef.current && filteredCommands.length > 0) setSlashHighlightIndex(0);
|
|
previousCommandQueryRef.current = commandQuery;
|
|
}, [commandQuery, filteredCommands.length]);
|
|
useEffect(() => {
|
|
if (commandQuery === null) {
|
|
setSlashHighlightIndex(0);
|
|
return;
|
|
}
|
|
if (filteredCommands.length === 0) setSlashHighlightIndex(-1);
|
|
else if (slashHighlightIndex < 0 || slashHighlightIndex >= filteredCommands.length) setSlashHighlightIndex(0);
|
|
}, [
|
|
commandQuery,
|
|
filteredCommands,
|
|
slashHighlightIndex
|
|
]);
|
|
useEffect(() => {
|
|
const recorder = audioRecorderRef.current;
|
|
if (!recorder) return;
|
|
if (mode === "transcribe") recorder.start().catch(console.error);
|
|
else if (recorder.state === "recording") recorder.stop().catch(console.error);
|
|
}, [mode]);
|
|
useEffect(() => {
|
|
if (mode !== "input") {
|
|
setLayout("compact");
|
|
setCommandQuery(null);
|
|
}
|
|
}, [mode]);
|
|
const updateSlashState = useCallback((value) => {
|
|
if (commandItems.length === 0) {
|
|
setCommandQuery((prev) => prev === null ? prev : null);
|
|
return;
|
|
}
|
|
if (value.startsWith("/")) {
|
|
const query = (value.split(/\r?\n/, 1)[0] ?? "").slice(1);
|
|
setCommandQuery((prev) => prev === query ? prev : query);
|
|
} else setCommandQuery((prev) => prev === null ? prev : null);
|
|
}, [commandItems.length]);
|
|
useEffect(() => {
|
|
updateSlashState(resolvedValue);
|
|
}, [resolvedValue, updateSlashState]);
|
|
const handleChange = (e) => {
|
|
const nextValue = e.target.value;
|
|
if (!isControlled) setInternalValue(nextValue);
|
|
onChange?.(nextValue);
|
|
updateSlashState(nextValue);
|
|
};
|
|
const clearInputValue = useCallback(() => {
|
|
if (!isControlled) setInternalValue("");
|
|
if (onChange) onChange("");
|
|
}, [isControlled, onChange]);
|
|
const runCommand = useCallback((item) => {
|
|
clearInputValue();
|
|
item.action?.();
|
|
setCommandQuery(null);
|
|
setSlashHighlightIndex(0);
|
|
requestAnimationFrame(() => {
|
|
inputRef.current?.focus();
|
|
});
|
|
}, [clearInputValue]);
|
|
const handleKeyDown = (e) => {
|
|
if (commandQuery !== null && mode === "input") {
|
|
if (e.key === "ArrowDown") {
|
|
if (filteredCommands.length > 0) {
|
|
e.preventDefault();
|
|
setSlashHighlightIndex((prev) => {
|
|
if (filteredCommands.length === 0) return prev;
|
|
return prev === -1 ? 0 : (prev + 1) % filteredCommands.length;
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
if (e.key === "ArrowUp") {
|
|
if (filteredCommands.length > 0) {
|
|
e.preventDefault();
|
|
setSlashHighlightIndex((prev) => {
|
|
if (filteredCommands.length === 0) return prev;
|
|
if (prev === -1) return filteredCommands.length - 1;
|
|
return prev <= 0 ? filteredCommands.length - 1 : prev - 1;
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
if (e.key === "Enter") {
|
|
const selected = slashHighlightIndex >= 0 ? filteredCommands[slashHighlightIndex] : void 0;
|
|
if (selected) {
|
|
e.preventDefault();
|
|
runCommand(selected);
|
|
return;
|
|
}
|
|
}
|
|
if (e.key === "Escape") {
|
|
e.preventDefault();
|
|
setCommandQuery(null);
|
|
return;
|
|
}
|
|
}
|
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
e.preventDefault();
|
|
if (isProcessing) onStop?.();
|
|
else send();
|
|
}
|
|
};
|
|
const send = () => {
|
|
if (!onSubmitMessage) return;
|
|
const trimmed = resolvedValue.trim();
|
|
if (!trimmed) return;
|
|
onSubmitMessage(trimmed);
|
|
if (!isControlled) {
|
|
setInternalValue("");
|
|
onChange?.("");
|
|
}
|
|
if (inputRef.current) inputRef.current.focus();
|
|
};
|
|
const BoundTextArea = renderSlot(textArea, CopilotChatInput.TextArea, {
|
|
ref: inputRef,
|
|
value: resolvedValue,
|
|
onChange: handleChange,
|
|
onKeyDown: handleKeyDown,
|
|
autoFocus,
|
|
className: twMerge("cpk:w-full cpk:py-3", isExpanded ? "cpk:px-5" : "cpk:pr-5")
|
|
});
|
|
const isProcessing = mode !== "transcribe" && isRunning;
|
|
const canSend = resolvedValue.trim().length > 0 && !!onSubmitMessage;
|
|
const canStop = !!onStop;
|
|
const handleSendButtonClick = () => {
|
|
if (isProcessing) {
|
|
onStop?.();
|
|
return;
|
|
}
|
|
send();
|
|
};
|
|
const BoundAudioRecorder = renderSlot(audioRecorder, CopilotChatAudioRecorder, { ref: audioRecorderRef });
|
|
const BoundSendButton = renderSlot(sendButton, CopilotChatInput.SendButton, {
|
|
onClick: handleSendButtonClick,
|
|
disabled: isProcessing ? !canStop : !canSend,
|
|
children: isProcessing && canStop ? /* @__PURE__ */ jsx(Square, { className: "cpk:size-[18px] cpk:fill-current" }) : void 0
|
|
});
|
|
const BoundStartTranscribeButton = renderSlot(startTranscribeButton, CopilotChatInput.StartTranscribeButton, { onClick: onStartTranscribe });
|
|
const BoundCancelTranscribeButton = renderSlot(cancelTranscribeButton, CopilotChatInput.CancelTranscribeButton, { onClick: onCancelTranscribe });
|
|
const handleFinishTranscribe = useCallback(async () => {
|
|
const recorder = audioRecorderRef.current;
|
|
if (recorder && recorder.state === "recording") try {
|
|
const audioBlob = await recorder.stop();
|
|
if (onFinishTranscribeWithAudio) await onFinishTranscribeWithAudio(audioBlob);
|
|
} catch (error) {
|
|
console.error("Failed to stop recording:", error);
|
|
}
|
|
onFinishTranscribe?.();
|
|
}, [onFinishTranscribe, onFinishTranscribeWithAudio]);
|
|
const BoundFinishTranscribeButton = renderSlot(finishTranscribeButton, CopilotChatInput.FinishTranscribeButton, { onClick: handleFinishTranscribe });
|
|
const BoundAddMenuButton = renderSlot(addMenuButton, CopilotChatInput.AddMenuButton, {
|
|
disabled: mode === "transcribe",
|
|
onAddFile,
|
|
toolsMenu
|
|
});
|
|
const BoundDisclaimer = renderSlot(disclaimer, CopilotChatInput.Disclaimer, {});
|
|
const shouldShowDisclaimer = showDisclaimer ?? positioning === "absolute";
|
|
if (children) {
|
|
const childProps = {
|
|
textArea: BoundTextArea,
|
|
audioRecorder: BoundAudioRecorder,
|
|
sendButton: BoundSendButton,
|
|
startTranscribeButton: BoundStartTranscribeButton,
|
|
cancelTranscribeButton: BoundCancelTranscribeButton,
|
|
finishTranscribeButton: BoundFinishTranscribeButton,
|
|
addMenuButton: BoundAddMenuButton,
|
|
disclaimer: BoundDisclaimer,
|
|
onSubmitMessage,
|
|
onStop,
|
|
isRunning,
|
|
onStartTranscribe,
|
|
onCancelTranscribe,
|
|
onFinishTranscribe,
|
|
onAddFile,
|
|
mode,
|
|
toolsMenu,
|
|
autoFocus,
|
|
positioning,
|
|
keyboardHeight,
|
|
showDisclaimer: shouldShowDisclaimer
|
|
};
|
|
return /* @__PURE__ */ jsx("div", {
|
|
"data-copilotkit": true,
|
|
style: { display: "contents" },
|
|
children: children(childProps)
|
|
});
|
|
}
|
|
const handleContainerClick = (e) => {
|
|
const target = e.target;
|
|
if (target.tagName !== "BUTTON" && !target.closest("button") && inputRef.current && mode === "input") inputRef.current.focus();
|
|
};
|
|
const ensureMeasurements = useCallback(() => {
|
|
const textarea = inputRef.current;
|
|
if (!textarea) return;
|
|
const previousValue = textarea.value;
|
|
const previousHeight = textarea.style.height;
|
|
textarea.style.height = "auto";
|
|
const computedStyle = window.getComputedStyle(textarea);
|
|
const paddingLeft = parseFloat(computedStyle.paddingLeft) || 0;
|
|
const paddingRight = parseFloat(computedStyle.paddingRight) || 0;
|
|
const paddingTop = parseFloat(computedStyle.paddingTop) || 0;
|
|
const paddingBottom = parseFloat(computedStyle.paddingBottom) || 0;
|
|
textarea.value = "";
|
|
const singleLineHeight = textarea.scrollHeight;
|
|
textarea.value = previousValue;
|
|
const maxHeight = (singleLineHeight - paddingTop - paddingBottom) * 5 + paddingTop + paddingBottom;
|
|
measurementsRef.current = {
|
|
singleLineHeight,
|
|
maxHeight,
|
|
paddingLeft,
|
|
paddingRight
|
|
};
|
|
textarea.style.height = previousHeight;
|
|
textarea.style.maxHeight = `${maxHeight}px`;
|
|
}, []);
|
|
const adjustTextareaHeight = useCallback(() => {
|
|
const textarea = inputRef.current;
|
|
if (!textarea) return 0;
|
|
if (measurementsRef.current.singleLineHeight === 0) ensureMeasurements();
|
|
const { maxHeight } = measurementsRef.current;
|
|
if (maxHeight) textarea.style.maxHeight = `${maxHeight}px`;
|
|
textarea.style.height = "auto";
|
|
const scrollHeight = textarea.scrollHeight;
|
|
if (maxHeight) textarea.style.height = `${Math.min(scrollHeight, maxHeight)}px`;
|
|
else textarea.style.height = `${scrollHeight}px`;
|
|
return scrollHeight;
|
|
}, [ensureMeasurements]);
|
|
const updateLayout = useCallback((nextLayout) => {
|
|
setLayout((prev) => {
|
|
if (prev === nextLayout) return prev;
|
|
ignoreResizeRef.current = true;
|
|
return nextLayout;
|
|
});
|
|
}, []);
|
|
const evaluateLayout = useCallback(() => {
|
|
if (mode !== "input") {
|
|
updateLayout("compact");
|
|
return;
|
|
}
|
|
if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
|
|
if (window.matchMedia("(max-width: 767px)").matches) {
|
|
ensureMeasurements();
|
|
adjustTextareaHeight();
|
|
updateLayout("expanded");
|
|
return;
|
|
}
|
|
}
|
|
const textarea = inputRef.current;
|
|
const grid = gridRef.current;
|
|
const addContainer = addButtonContainerRef.current;
|
|
const actionsContainer = actionsContainerRef.current;
|
|
if (!textarea || !grid || !addContainer || !actionsContainer) return;
|
|
if (measurementsRef.current.singleLineHeight === 0) ensureMeasurements();
|
|
const scrollHeight = adjustTextareaHeight();
|
|
const baseline = measurementsRef.current.singleLineHeight;
|
|
const hasExplicitBreak = resolvedValue.includes("\n");
|
|
const renderedMultiline = baseline > 0 ? scrollHeight > baseline + 1 : false;
|
|
let shouldExpand = hasExplicitBreak || renderedMultiline;
|
|
if (!shouldExpand) {
|
|
const gridStyles = window.getComputedStyle(grid);
|
|
const paddingLeft = parseFloat(gridStyles.paddingLeft) || 0;
|
|
const paddingRight = parseFloat(gridStyles.paddingRight) || 0;
|
|
const columnGap = parseFloat(gridStyles.columnGap) || 0;
|
|
const gridAvailableWidth = grid.clientWidth - paddingLeft - paddingRight;
|
|
if (gridAvailableWidth > 0) {
|
|
const addWidth = addContainer.getBoundingClientRect().width;
|
|
const actionsWidth = actionsContainer.getBoundingClientRect().width;
|
|
const compactWidth = Math.max(gridAvailableWidth - addWidth - actionsWidth - columnGap * 2, 0);
|
|
const canvas = measurementCanvasRef.current ?? document.createElement("canvas");
|
|
if (!measurementCanvasRef.current) measurementCanvasRef.current = canvas;
|
|
const context = canvas.getContext("2d");
|
|
if (context) {
|
|
const textareaStyles = window.getComputedStyle(textarea);
|
|
context.font = textareaStyles.font || `${textareaStyles.fontStyle} ${textareaStyles.fontVariant} ${textareaStyles.fontWeight} ${textareaStyles.fontSize}/${textareaStyles.lineHeight} ${textareaStyles.fontFamily}`;
|
|
const compactInnerWidth = Math.max(compactWidth - (measurementsRef.current.paddingLeft || 0) - (measurementsRef.current.paddingRight || 0), 0);
|
|
if (compactInnerWidth > 0) {
|
|
const lines = resolvedValue.length > 0 ? resolvedValue.split("\n") : [""];
|
|
let longestWidth = 0;
|
|
for (const line of lines) {
|
|
const metrics = context.measureText(line || " ");
|
|
if (metrics.width > longestWidth) longestWidth = metrics.width;
|
|
}
|
|
if (longestWidth > compactInnerWidth) shouldExpand = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
updateLayout(shouldExpand ? "expanded" : "compact");
|
|
}, [
|
|
adjustTextareaHeight,
|
|
ensureMeasurements,
|
|
mode,
|
|
resolvedValue,
|
|
updateLayout
|
|
]);
|
|
useLayoutEffect(() => {
|
|
evaluateLayout();
|
|
}, [evaluateLayout]);
|
|
useEffect(() => {
|
|
if (typeof ResizeObserver === "undefined") return;
|
|
const textarea = inputRef.current;
|
|
const grid = gridRef.current;
|
|
const addContainer = addButtonContainerRef.current;
|
|
const actionsContainer = actionsContainerRef.current;
|
|
if (!textarea || !grid || !addContainer || !actionsContainer) return;
|
|
const scheduleEvaluation = () => {
|
|
if (ignoreResizeRef.current) {
|
|
ignoreResizeRef.current = false;
|
|
return;
|
|
}
|
|
if (typeof window === "undefined") {
|
|
evaluateLayout();
|
|
return;
|
|
}
|
|
if (resizeEvaluationRafRef.current !== null) cancelAnimationFrame(resizeEvaluationRafRef.current);
|
|
resizeEvaluationRafRef.current = window.requestAnimationFrame(() => {
|
|
resizeEvaluationRafRef.current = null;
|
|
evaluateLayout();
|
|
});
|
|
};
|
|
const observer = new ResizeObserver(() => {
|
|
scheduleEvaluation();
|
|
});
|
|
observer.observe(grid);
|
|
observer.observe(addContainer);
|
|
observer.observe(actionsContainer);
|
|
observer.observe(textarea);
|
|
return () => {
|
|
observer.disconnect();
|
|
if (typeof window !== "undefined" && resizeEvaluationRafRef.current !== null) {
|
|
cancelAnimationFrame(resizeEvaluationRafRef.current);
|
|
resizeEvaluationRafRef.current = null;
|
|
}
|
|
};
|
|
}, [evaluateLayout]);
|
|
const slashMenuVisible = commandQuery !== null && commandItems.length > 0;
|
|
useEffect(() => {
|
|
if (!slashMenuVisible || slashHighlightIndex < 0) return;
|
|
(slashMenuRef.current?.querySelector(`[data-slash-index="${slashHighlightIndex}"]`))?.scrollIntoView({ block: "nearest" });
|
|
}, [slashMenuVisible, slashHighlightIndex]);
|
|
const slashMenu = slashMenuVisible ? /* @__PURE__ */ jsx("div", {
|
|
"data-testid": "copilot-slash-menu",
|
|
role: "listbox",
|
|
"aria-label": "Slash commands",
|
|
ref: slashMenuRef,
|
|
className: "cpk:absolute cpk:bottom-full cpk:left-0 cpk:right-0 cpk:z-30 cpk:mb-2 cpk:max-h-64 cpk:overflow-y-auto cpk:rounded-lg cpk:border cpk:border-border cpk:bg-white cpk:shadow-lg cpk:dark:border-[#3a3a3a] cpk:dark:bg-[#1f1f1f]",
|
|
style: { maxHeight: `${SLASH_MENU_MAX_VISIBLE_ITEMS * SLASH_MENU_ITEM_HEIGHT_PX}px` },
|
|
children: filteredCommands.length === 0 ? /* @__PURE__ */ jsx("div", {
|
|
className: "cpk:px-3 cpk:py-2 cpk:text-sm cpk:text-muted-foreground",
|
|
children: "No commands found"
|
|
}) : filteredCommands.map((item, index) => {
|
|
const isActive = index === slashHighlightIndex;
|
|
return /* @__PURE__ */ jsx("button", {
|
|
type: "button",
|
|
role: "option",
|
|
"aria-selected": isActive,
|
|
"data-active": isActive ? "true" : void 0,
|
|
"data-slash-index": index,
|
|
className: twMerge("cpk:w-full cpk:px-3 cpk:py-2 cpk:text-left cpk:text-sm cpk:transition-colors", "cpk:hover:bg-muted cpk:dark:hover:bg-[#2f2f2f]", isActive ? "cpk:bg-muted cpk:dark:bg-[#2f2f2f]" : "cpk:bg-transparent"),
|
|
onMouseEnter: () => setSlashHighlightIndex(index),
|
|
onMouseDown: (event) => {
|
|
event.preventDefault();
|
|
runCommand(item);
|
|
},
|
|
children: item.label
|
|
}, `${item.label}-${index}`);
|
|
})
|
|
}) : null;
|
|
const inputPill = /* @__PURE__ */ jsx("div", {
|
|
"data-testid": "copilot-chat-input",
|
|
className: twMerge("copilotKitInput", "cpk:flex cpk:w-full cpk:flex-col cpk:items-center cpk:justify-center", "cpk:cursor-text", "cpk:overflow-visible cpk:bg-clip-padding cpk:contain-inline-size", "cpk:bg-white cpk:dark:bg-[#303030]", "cpk:shadow-[0_4px_4px_0_#0000000a,0_0_1px_0_#0000009e] cpk:rounded-[28px]"),
|
|
onClick: handleContainerClick,
|
|
"data-layout": isExpanded ? "expanded" : "compact",
|
|
children: /* @__PURE__ */ jsxs("div", {
|
|
ref: gridRef,
|
|
className: twMerge("cpk:grid cpk:w-full cpk:gap-x-3 cpk:gap-y-3 cpk:px-3 cpk:py-2", isExpanded ? "cpk:grid-cols-[auto_minmax(0,1fr)_auto] cpk:grid-rows-[auto_auto]" : "cpk:grid-cols-[auto_minmax(0,1fr)_auto] cpk:items-center"),
|
|
"data-layout": isExpanded ? "expanded" : "compact",
|
|
children: [
|
|
/* @__PURE__ */ jsx("div", {
|
|
ref: addButtonContainerRef,
|
|
className: twMerge("cpk:flex cpk:items-center", isExpanded ? "cpk:row-start-2" : "cpk:row-start-1", "cpk:col-start-1"),
|
|
children: BoundAddMenuButton
|
|
}),
|
|
/* @__PURE__ */ jsx("div", {
|
|
className: twMerge("cpk:relative cpk:flex cpk:min-w-0 cpk:flex-col cpk:min-h-[50px] cpk:justify-center", isExpanded ? "cpk:col-span-3 cpk:row-start-1" : "cpk:col-start-2 cpk:row-start-1"),
|
|
children: mode === "transcribe" ? BoundAudioRecorder : mode === "processing" ? /* @__PURE__ */ jsx("div", {
|
|
className: "cpk:flex cpk:w-full cpk:items-center cpk:justify-center cpk:py-3 cpk:px-5",
|
|
children: /* @__PURE__ */ jsx(Loader2, { className: "cpk:size-[26px] cpk:animate-spin cpk:text-muted-foreground" })
|
|
}) : /* @__PURE__ */ jsxs(Fragment, { children: [BoundTextArea, slashMenu] })
|
|
}),
|
|
/* @__PURE__ */ jsx("div", {
|
|
ref: actionsContainerRef,
|
|
className: twMerge("cpk:flex cpk:items-center cpk:justify-end cpk:gap-2", isExpanded ? "cpk:col-start-3 cpk:row-start-2" : "cpk:col-start-3 cpk:row-start-1"),
|
|
children: mode === "transcribe" ? /* @__PURE__ */ jsxs(Fragment, { children: [onCancelTranscribe && BoundCancelTranscribeButton, onFinishTranscribe && BoundFinishTranscribeButton] }) : /* @__PURE__ */ jsxs(Fragment, { children: [onStartTranscribe && BoundStartTranscribeButton, BoundSendButton] })
|
|
})
|
|
]
|
|
})
|
|
});
|
|
return /* @__PURE__ */ jsxs("div", {
|
|
"data-copilotkit": true,
|
|
ref: containerRef,
|
|
className: cn(positioning === "absolute" && "cpk:absolute cpk:bottom-0 cpk:left-0 cpk:right-0 cpk:z-20 cpk:pointer-events-none", className),
|
|
style: {
|
|
transform: keyboardHeight > 0 ? `translateY(-${keyboardHeight}px)` : void 0,
|
|
transition: "transform 0.2s ease-out"
|
|
},
|
|
...props,
|
|
children: [/* @__PURE__ */ jsx("div", {
|
|
className: "cpk:max-w-3xl cpk:mx-auto cpk:py-0 cpk:px-4 cpk:sm:px-0 cpk:[div[data-sidebar-chat]_&]:px-8 cpk:[div[data-popup-chat]_&]:px-4 cpk:pointer-events-auto",
|
|
children: inputPill
|
|
}), shouldShowDisclaimer && BoundDisclaimer]
|
|
});
|
|
}
|
|
(function(_CopilotChatInput) {
|
|
_CopilotChatInput.SendButton = ({ className, children, ...props }) => /* @__PURE__ */ jsx("div", {
|
|
className: "cpk:mr-[10px]",
|
|
children: /* @__PURE__ */ jsx(Button, {
|
|
type: "button",
|
|
"data-testid": "copilot-send-button",
|
|
variant: "chatInputToolbarPrimary",
|
|
size: "chatInputToolbarIcon",
|
|
className,
|
|
...props,
|
|
children: children ?? /* @__PURE__ */ jsx(ArrowUp, { className: "cpk:size-[18px]" })
|
|
})
|
|
});
|
|
const ToolbarButton = _CopilotChatInput.ToolbarButton = ({ icon, labelKey, defaultClassName, className, ...props }) => {
|
|
const labels = useCopilotChatConfiguration()?.labels ?? CopilotChatDefaultLabels;
|
|
return /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
|
|
asChild: true,
|
|
children: /* @__PURE__ */ jsx(Button, {
|
|
type: "button",
|
|
variant: "chatInputToolbarSecondary",
|
|
size: "chatInputToolbarIcon",
|
|
className: twMerge(defaultClassName, className),
|
|
...props,
|
|
children: icon
|
|
})
|
|
}), /* @__PURE__ */ jsx(TooltipContent, {
|
|
side: "bottom",
|
|
children: /* @__PURE__ */ jsx("p", { children: labels[labelKey] })
|
|
})] });
|
|
};
|
|
_CopilotChatInput.StartTranscribeButton = (props) => /* @__PURE__ */ jsx(ToolbarButton, {
|
|
"data-testid": "copilot-start-transcribe-button",
|
|
icon: /* @__PURE__ */ jsx(Mic, { className: "cpk:size-[18px]" }),
|
|
labelKey: "chatInputToolbarStartTranscribeButtonLabel",
|
|
defaultClassName: "cpk:mr-2",
|
|
...props
|
|
});
|
|
_CopilotChatInput.CancelTranscribeButton = (props) => /* @__PURE__ */ jsx(ToolbarButton, {
|
|
"data-testid": "copilot-cancel-transcribe-button",
|
|
icon: /* @__PURE__ */ jsx(X, { className: "cpk:size-[18px]" }),
|
|
labelKey: "chatInputToolbarCancelTranscribeButtonLabel",
|
|
defaultClassName: "cpk:mr-2",
|
|
...props
|
|
});
|
|
_CopilotChatInput.FinishTranscribeButton = (props) => /* @__PURE__ */ jsx(ToolbarButton, {
|
|
"data-testid": "copilot-finish-transcribe-button",
|
|
icon: /* @__PURE__ */ jsx(Check, { className: "cpk:size-[18px]" }),
|
|
labelKey: "chatInputToolbarFinishTranscribeButtonLabel",
|
|
defaultClassName: "cpk:mr-[10px]",
|
|
...props
|
|
});
|
|
_CopilotChatInput.AddMenuButton = ({ className, toolsMenu, onAddFile, disabled, ...props }) => {
|
|
const labels = useCopilotChatConfiguration()?.labels ?? CopilotChatDefaultLabels;
|
|
const menuItems = useMemo(() => {
|
|
const items = [];
|
|
if (onAddFile) items.push({
|
|
label: labels.chatInputToolbarAddButtonLabel,
|
|
action: onAddFile
|
|
});
|
|
if (toolsMenu && toolsMenu.length > 0) {
|
|
if (items.length > 0) items.push("-");
|
|
for (const item of toolsMenu) if (item === "-") {
|
|
if (items.length === 0 || items[items.length - 1] === "-") continue;
|
|
items.push(item);
|
|
} else items.push(item);
|
|
while (items.length > 0 && items[items.length - 1] === "-") items.pop();
|
|
}
|
|
return items;
|
|
}, [
|
|
onAddFile,
|
|
toolsMenu,
|
|
labels.chatInputToolbarAddButtonLabel
|
|
]);
|
|
const renderMenuItems = useCallback((items) => items.map((item, index) => {
|
|
if (item === "-") return /* @__PURE__ */ jsx(DropdownMenuSeparator, {}, `separator-${index}`);
|
|
if (item.items && item.items.length > 0) return /* @__PURE__ */ jsxs(DropdownMenuSub, { children: [/* @__PURE__ */ jsx(DropdownMenuSubTrigger, { children: item.label }), /* @__PURE__ */ jsx(DropdownMenuSubContent, { children: renderMenuItems(item.items) })] }, `group-${index}`);
|
|
return /* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
onClick: item.action,
|
|
children: item.label
|
|
}, `item-${index}`);
|
|
}), []);
|
|
const hasMenuItems = menuItems.length > 0;
|
|
const isDisabled = disabled || !hasMenuItems;
|
|
return /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
|
|
asChild: true,
|
|
children: /* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
asChild: true,
|
|
children: /* @__PURE__ */ jsx(Button, {
|
|
type: "button",
|
|
"data-testid": "copilot-add-menu-button",
|
|
variant: "chatInputToolbarSecondary",
|
|
size: "chatInputToolbarIcon",
|
|
className: twMerge("cpk:ml-1", className),
|
|
disabled: isDisabled,
|
|
...props,
|
|
children: /* @__PURE__ */ jsx(Plus, { className: "cpk:size-[20px]" })
|
|
})
|
|
})
|
|
}), /* @__PURE__ */ jsx(TooltipContent, {
|
|
side: "bottom",
|
|
children: /* @__PURE__ */ jsxs("p", {
|
|
className: "cpk:flex cpk:items-center cpk:gap-1 cpk:text-xs cpk:font-medium",
|
|
children: [/* @__PURE__ */ jsx("span", { children: "Add files and more" }), /* @__PURE__ */ jsx("code", {
|
|
className: "cpk:rounded cpk:bg-[#4a4a4a] cpk:px-1 cpk:py-[1px] cpk:font-mono cpk:text-[11px] cpk:text-white cpk:dark:bg-[#e0e0e0] cpk:dark:text-black",
|
|
children: "/"
|
|
})]
|
|
})
|
|
})] }), hasMenuItems && /* @__PURE__ */ jsx(DropdownMenuContent, {
|
|
side: "top",
|
|
align: "start",
|
|
children: renderMenuItems(menuItems)
|
|
})] });
|
|
};
|
|
_CopilotChatInput.TextArea = forwardRef(function TextArea({ style, className, autoFocus, placeholder, ...props }, ref) {
|
|
const internalTextareaRef = useRef(null);
|
|
const labels = useCopilotChatConfiguration()?.labels ?? CopilotChatDefaultLabels;
|
|
useImperativeHandle(ref, () => internalTextareaRef.current);
|
|
useEffect(() => {
|
|
const textarea = internalTextareaRef.current;
|
|
if (!textarea) return;
|
|
const handleFocus = () => {
|
|
setTimeout(() => {
|
|
textarea.scrollIntoView({
|
|
behavior: "smooth",
|
|
block: "nearest"
|
|
});
|
|
}, 300);
|
|
};
|
|
textarea.addEventListener("focus", handleFocus);
|
|
return () => textarea.removeEventListener("focus", handleFocus);
|
|
}, []);
|
|
useEffect(() => {
|
|
if (autoFocus) internalTextareaRef.current?.focus();
|
|
}, [autoFocus]);
|
|
return /* @__PURE__ */ jsx("textarea", {
|
|
ref: internalTextareaRef,
|
|
"data-testid": "copilot-chat-textarea",
|
|
placeholder: placeholder ?? labels.chatInputPlaceholder,
|
|
className: twMerge("cpk:bg-transparent cpk:outline-none cpk:antialiased cpk:font-regular cpk:leading-relaxed cpk:text-[16px] cpk:placeholder:text-[#00000077] cpk:dark:placeholder:text-[#fffc]", className),
|
|
style: {
|
|
overflow: "auto",
|
|
resize: "none",
|
|
...style
|
|
},
|
|
rows: 1,
|
|
...props
|
|
});
|
|
});
|
|
_CopilotChatInput.AudioRecorder = CopilotChatAudioRecorder;
|
|
_CopilotChatInput.Disclaimer = ({ className, ...props }) => {
|
|
const labels = useCopilotChatConfiguration()?.labels ?? CopilotChatDefaultLabels;
|
|
return /* @__PURE__ */ jsx("div", {
|
|
className: cn("cpk:text-center cpk:text-xs cpk:text-muted-foreground cpk:py-3 cpk:px-4 cpk:max-w-3xl cpk:mx-auto", className),
|
|
...props,
|
|
children: labels.chatDisclaimerText
|
|
});
|
|
};
|
|
})(CopilotChatInput || (CopilotChatInput = {}));
|
|
CopilotChatInput.TextArea.displayName = "CopilotChatInput.TextArea";
|
|
CopilotChatInput.SendButton.displayName = "CopilotChatInput.SendButton";
|
|
CopilotChatInput.ToolbarButton.displayName = "CopilotChatInput.ToolbarButton";
|
|
CopilotChatInput.StartTranscribeButton.displayName = "CopilotChatInput.StartTranscribeButton";
|
|
CopilotChatInput.CancelTranscribeButton.displayName = "CopilotChatInput.CancelTranscribeButton";
|
|
CopilotChatInput.FinishTranscribeButton.displayName = "CopilotChatInput.FinishTranscribeButton";
|
|
CopilotChatInput.AddMenuButton.displayName = "CopilotChatInput.AddMenuButton";
|
|
CopilotChatInput.Disclaimer.displayName = "CopilotChatInput.Disclaimer";
|
|
var CopilotChatInput_default = CopilotChatInput;
|
|
|
|
//#endregion
|
|
export { CopilotChatInput_default as default };
|
|
//# sourceMappingURL=CopilotChatInput.mjs.map
|