2025-10-26 09:15:11 +00:00
< script lang = "ts" >
2026-03-01 12:37:18 +00:00
import { onDestroy , onMount , tick } from "svelte";
2025-11-27 12:39:08 +00:00
import { Platform , setIcon , type EventRef } from "obsidian";
2025-10-29 19:35:19 +00:00
import type { UserInputService } from "Services/UserInputService";
import type { ISearchState , SearchStateStore } from "Stores/SearchStateStore";
import { Resolve } from "Services/DependencyService";
import { Services } from "Services/Services";
2025-12-11 12:41:14 +00:00
import { isSearchTrigger , fromInput , toNode , triggerToText } from "Enums/SearchTrigger";
2025-10-29 19:35:19 +00:00
import ChatSearchResults from "./ChatSearchResults.svelte";
import type { Writable } from "svelte/store";
2025-10-29 19:46:20 +00:00
import type { InputService } from "Services/InputService";
2025-11-02 20:16:06 +00:00
import UserInstruction from "./UserInstruction.svelte";
2026-04-19 19:39:04 +00:00
import ChatModeSelector from "./ChatModeSelector.svelte";
2025-11-27 12:39:08 +00:00
import DiffControls from "./DiffControls.svelte";
import type { EventService } from "Services/EventService";
import { Event } from "Enums/Event";
import type { DiffService } from "Services/DiffService";
2025-12-22 20:02:02 +00:00
import type { Attachment } from "Conversations/Attachment";
import ChatAttachments from "./ChatAttachments.svelte";
2026-01-04 18:52:44 +00:00
import InputDisplay from "./InputDisplay.svelte";
import { InputMode } from "Enums/InputMode";
2026-01-05 21:49:51 +00:00
import { Copy , replaceCopy } from "Enums/Copy";
import { HelpModal } from "Modals/HelpModal";
2026-03-01 12:37:18 +00:00
import type { IPrompt } from "AIPrompts/IPrompt";
2026-04-10 09:47:23 +00:00
import type { SettingsService } from "Services/SettingsService";
2026-04-19 19:39:04 +00:00
import { ChatMode , chatModeAllowsEdits , iconForChatMode } from "Enums/ChatMode";
2025-12-22 20:02:02 +00:00
export let attachments: Attachment[] = [];
2025-10-26 09:15:11 +00:00
export let hasNoApiKey: boolean;
export let isSubmitting: boolean;
2026-04-19 19:39:04 +00:00
export let chatMode: ChatMode;
2026-01-03 11:15:32 +00:00
export let onSubmit: (userRequest: string, formattedRequest: string) => void;
export let onStop: () => void;
2025-10-26 09:15:11 +00:00
2025-10-29 19:46:20 +00:00
const inputService: InputService = Resolve< InputService > (Services.InputService);
2026-04-10 09:47:23 +00:00
const settingsService: SettingsService = Resolve< SettingsService > (Services.SettingsService);
2025-10-29 19:35:19 +00:00
const userInputService: UserInputService = Resolve< UserInputService > (Services.UserInputService);
const searchStateStore: SearchStateStore = Resolve< SearchStateStore > (Services.SearchStateStore);
2025-11-27 12:39:08 +00:00
const diffService: DiffService = Resolve< DiffService > (Services.DiffService);
const eventService: EventService = Resolve< EventService > (Services.EventService);
2026-03-01 12:37:18 +00:00
const aiPrompt: IPrompt = Resolve< IPrompt > (Services.IPrompt);
2025-10-29 19:35:19 +00:00
const searchState: Writable< ISearchState > = searchStateStore.searchState;
2026-01-04 18:52:44 +00:00
let inputDisplay: InputDisplay;
2025-10-26 09:15:11 +00:00
let textareaElement: HTMLDivElement;
2025-11-02 20:16:06 +00:00
let userInstructionButton: HTMLButtonElement;
2026-04-10 09:47:23 +00:00
let webSearchButton: HTMLButtonElement;
2025-10-26 09:15:11 +00:00
let submitButton: HTMLButtonElement;
2026-04-19 19:39:04 +00:00
let attachmentButton: HTMLButtonElement;
let chatModeButton: HTMLButtonElement;
2025-11-02 20:16:06 +00:00
2026-04-19 19:39:04 +00:00
let chatModeSelectionAreaActive: boolean = false;
2026-03-01 12:37:18 +00:00
let userInstructionAreaActive: boolean = false;
let userInstructionActive: boolean = true;
2026-03-22 20:07:26 +00:00
let stacked: boolean = false;
2025-12-22 20:02:02 +00:00
let userRequest: string = "";
2025-10-26 09:15:11 +00:00
2026-01-04 18:52:44 +00:00
let inputMode: InputMode = InputMode.Normal;
let questionResolver: ((answer: string) => void) | null = null;
2026-01-05 21:49:51 +00:00
let countdownIntervalId: ReturnType< typeof setInterval > | null = null;
let countdownSecondsRemaining: number = 0;
2026-03-22 20:07:26 +00:00
let inputInitialHeight: number = 0;
2026-01-05 21:49:51 +00:00
2026-01-04 18:52:44 +00:00
const diffOpenedRef: EventRef = eventService.on(Event.DiffOpened, () => { inputMode = InputMode . Diff ; focusInput (); } );
const diffClosedRef: EventRef = eventService.on(Event.DiffClosed, () => { inputMode = InputMode . Normal ; focusInput (); } );
2026-01-05 21:49:51 +00:00
const rateLimitCountdownRef: EventRef = eventService.on(Event.RateLimitCountdown, (delayMs: number) => { startCountdown ( delayMs ); } );
2025-11-27 12:39:08 +00:00
2026-03-01 12:37:18 +00:00
onMount(async () => {
userInstructionActive = (await aiPrompt.userInstruction()).trim() !== "";
2026-03-22 20:07:26 +00:00
inputInitialHeight = textareaElement.innerHeight;
2026-03-01 12:37:18 +00:00
});
2025-11-27 12:39:08 +00:00
onDestroy(() => {
eventService.offref(diffOpenedRef);
eventService.offref(diffClosedRef);
2026-01-05 21:49:51 +00:00
eventService.offref(rateLimitCountdownRef);
stopCountdown();
2025-11-27 12:39:08 +00:00
});
2026-03-22 20:07:26 +00:00
function checkStacked() {
if (textareaElement.textContent.trim() === "") {
stacked = false;
return;
}
if (textareaElement.innerHeight > inputInitialHeight) {
stacked = true;
return;
}
}
2025-12-13 10:40:20 +00:00
export function focusInput(onMobile: boolean = false) {
// don't focus on mobile, it's annoying
if (onMobile || !Platform.isMobile) {
tick().then(() => {
textareaElement?.focus();
});
}
2025-10-26 09:15:11 +00:00
}
2026-01-04 18:52:44 +00:00
export function setDisplayItem(element: HTMLElement) {
inputDisplay.setDisplayItem(element);
}
export function clearDisplayItem() {
2026-01-05 21:49:51 +00:00
stopCountdown();
2026-01-04 18:52:44 +00:00
inputDisplay.clearDisplayItem();
inputMode = InputMode.Normal;
}
2026-01-05 21:49:51 +00:00
async function startCountdown(delayMs: number) {
stopCountdown();
countdownSecondsRemaining = Math.ceil(delayMs / 1000);
updateCountdownDisplay();
countdownIntervalId = setInterval(() => {
countdownSecondsRemaining--;
if (countdownSecondsRemaining < = 0) {
clearDisplayItem();
} else {
updateCountdownDisplay();
}
}, 1000);
}
function stopCountdown() {
if (countdownIntervalId !== null) {
clearInterval(countdownIntervalId);
countdownIntervalId = null;
}
}
function openTroubleshootingModal() {
const modal = Resolve< HelpModal > (Services.HelpModal);
modal.open(3); // 3 = Troubleshooting
}
function updateCountdownDisplay() {
const countdownDisplay = createEl("div");
countdownDisplay.addClass("rate-limit-container");
const countdown = createEl("span");
countdown.addClass("rate-limit-countdown");
countdown.textContent = replaceCopy(Copy.RateLimitCountdown, [countdownSecondsRemaining.toString()]);
const info1 = createEl("span");
info1.addClass("rate-limit-info");
info1.appendText(Copy.RateLimitInfo1);
const link = createEl("span");
link.addClass("rate-limit-link");
link.textContent = Copy.RateLimitInfoLink;
link.setAttribute("role", "link");
link.setAttribute("tabindex", "-1");
link.addEventListener("click", openTroubleshootingModal);
info1.append(link);
const info2 = createEl("span");
info2.addClass("rate-limit-info");
info2.appendText(Copy.RateLimitInfo2);
info1.append(info2);
countdownDisplay.append(countdown);
countdownDisplay.append(createEl("br"));
countdownDisplay.append(info1);
inputDisplay.setDisplayItem(countdownDisplay);
}
2026-01-04 18:52:44 +00:00
export function enterQuestionMode(resolver: (answer: string) => void) {
questionResolver = resolver;
inputMode = InputMode.Question;
focusInput();
}
2025-11-02 20:16:06 +00:00
$: if (userInstructionButton) {
setIcon(userInstructionButton, "user-round-pen");
}
2026-04-10 09:47:23 +00:00
$: if (webSearchButton) {
setIcon(webSearchButton, "globe");
}
2026-03-01 12:37:18 +00:00
$: userInstructionAreaActive, (() => {
tick().then(async () => {
userInstructionActive = (await aiPrompt.userInstruction()).trim() !== "";
});
})();
2025-10-29 19:35:19 +00:00
$: if (submitButton) {
2026-01-04 18:52:44 +00:00
if (inputMode === InputMode.Question || inputMode === InputMode.Diff) {
2025-12-05 23:34:52 +00:00
setIcon(submitButton, userRequest.trim() === "" ? "square" : "send-horizontal");
} else {
setIcon(submitButton, isSubmitting ? "square" : "send-horizontal");
}
2025-10-29 19:35:19 +00:00
}
2026-04-19 19:39:04 +00:00
$: if (attachmentButton) {
setIcon(attachmentButton, "paperclip");
2025-10-29 19:35:19 +00:00
}
2026-04-19 19:39:04 +00:00
$: if (chatModeButton) {
setIcon(chatModeButton, iconForChatMode(chatMode));
2026-01-04 18:52:44 +00:00
}
$: inputPlaceholder = (() => {
if (inputMode === InputMode.Question) return Copy.InputPlaceholderQuestion;
if (inputMode === InputMode.Diff) return Copy.InputPlaceholderDiff;
return Copy.InputPlaceholderNormal;
})();
$: submitDisabled = (() => {
if (inputMode === InputMode.Diff || inputMode === InputMode.Question) {
return false;
}
return !isSubmitting & & userRequest.trim() === "";
})();
$: submitAriaLabel = (() => {
if (inputMode === InputMode.Question) {
return userRequest.trim() === "" ? Copy.ButtonCancel : Copy.ButtonSubmitAnswer;
}
if (inputMode === InputMode.Diff) {
return userRequest.trim() === "" ? Copy.ButtonCancel : Copy.ButtonMakeSuggestion;
}
return isSubmitting ? Copy.ButtonCancel : Copy.ButtonSendMessage;
})();
2025-10-26 09:15:11 +00:00
function handleStop() {
2026-01-03 11:15:32 +00:00
onStop();
2025-10-26 09:15:11 +00:00
}
function handleSubmit() {
if (userRequest.trim() === "" || isSubmitting) {
return;
}
2025-11-27 12:39:08 +00:00
const result = requestFromInput();
2026-01-03 11:15:32 +00:00
onSubmit(result.request, result.formattedRequest);
2025-11-27 12:39:08 +00:00
}
2025-10-26 09:15:11 +00:00
2025-11-27 12:39:08 +00:00
function handleSuggestion() {
2026-01-04 18:52:44 +00:00
if (userRequest.trim() === "" || inputMode !== InputMode.Diff) {
2025-11-27 12:39:08 +00:00
return;
}
2025-12-23 13:39:52 +00:00
const suggestion = requestFromInput();
diffService.onSuggest(suggestion.formattedRequest);
2025-11-27 12:39:08 +00:00
}
2026-01-04 18:52:44 +00:00
function handleAnswer() {
if (userRequest.trim() === "" || inputMode !== InputMode.Question) {
return;
}
const answer = requestFromInput();
if (questionResolver) {
questionResolver(answer.formattedRequest);
questionResolver = null;
}
clearDisplayItem();
}
2025-11-27 12:39:08 +00:00
function requestFromInput() {
2025-10-31 15:03:40 +00:00
const request = textareaElement.innerHTML;
2025-11-10 12:59:48 +00:00
const formattedRequest = triggerToText(request);
2025-10-31 15:03:40 +00:00
2025-10-26 09:15:11 +00:00
textareaElement.textContent = "";
userRequest = "";
2026-03-22 20:07:26 +00:00
checkStacked();
2025-10-26 09:15:11 +00:00
2025-11-12 00:06:54 +00:00
if (Platform.isMobile) {
textareaElement.blur();
} else {
focusInput();
}
2025-11-27 12:39:08 +00:00
return { request : request , formattedRequest : formattedRequest } ;
2025-10-26 09:15:11 +00:00
}
2026-03-01 12:37:18 +00:00
function toggleUserInstructionArea() {
userInstructionAreaActive = !userInstructionAreaActive;
searchStateStore.resetSearch();
}
2026-04-10 09:47:23 +00:00
function toggleWebSearch() {
settingsService.settings.enableWebSearch = !settingsService.settings.enableWebSearch;
settingsService.saveSettings();
}
2026-04-19 19:39:04 +00:00
function toggleChatModeSelectionArea() {
chatModeSelectionAreaActive = !chatModeSelectionAreaActive;
2025-10-26 09:15:11 +00:00
}
2025-10-29 19:35:19 +00:00
async function handleKeydown(e: KeyboardEvent) {
2026-03-01 12:37:18 +00:00
userInstructionAreaActive = false;
2025-10-29 19:35:19 +00:00
if ($searchState.active) {
await continueSearch(e);
return;
}
if (e.key === "Enter") {
2025-11-12 22:05:12 +00:00
if (e.shiftKey || Platform.isMobile) {
2025-10-26 09:15:11 +00:00
return;
2025-10-29 19:35:19 +00:00
}
e.preventDefault();
2026-01-04 18:52:44 +00:00
if (inputMode === InputMode.Question) {
handleAnswer();
} else if (inputMode === InputMode.Diff) {
handleSuggestion();
} else {
handleSubmit();
}
2025-10-29 19:35:19 +00:00
}
2025-11-12 22:05:12 +00:00
}
2025-10-29 19:35:19 +00:00
2025-11-12 22:05:12 +00:00
// Detect search triggers on character insertion
function handleBeforeInput(e: InputEvent) {
// This works reliably on both desktop and mobile (including virtual keyboards)
if (e.inputType === "insertText" && e.data && isSearchTrigger(e.data)) {
2025-10-29 21:33:45 +00:00
const position = inputService.getCursorPosition(textareaElement);
2025-11-12 22:05:12 +00:00
const trigger = fromInput(e.data);
2025-10-29 19:35:19 +00:00
searchStateStore.initializeSearch(trigger, position);
}
2025-11-12 22:05:12 +00:00
// Let the character insert, handleInput() will synchronize the search query
2025-10-29 19:35:19 +00:00
}
async function continueSearch(e: KeyboardEvent) {
2025-10-31 15:03:40 +00:00
if (!$searchState.trigger) {
searchStateStore.resetSearch();
return;
}
2025-10-29 19:35:19 +00:00
if (e.key === "Escape") {
searchStateStore.resetSearch();
e.preventDefault();
return;
}
if (e.key === "Enter") {
2025-10-31 15:03:40 +00:00
e.preventDefault();
2025-11-02 20:16:06 +00:00
handleSearchResultAcceptance();
2025-10-29 19:35:19 +00:00
return;
}
if (e.key === "Backspace" || e.key === "Delete") {
return;
}
if (e.key.startsWith("Arrow")) {
if (e.key === "ArrowUp") {
2025-10-26 09:15:11 +00:00
e.preventDefault();
2025-10-29 19:35:19 +00:00
searchStateStore.setSelectedResultToPrevious();
2025-10-26 09:15:11 +00:00
}
2025-10-29 19:35:19 +00:00
if (e.key === "ArrowDown") {
e.preventDefault();
2025-10-29 21:33:45 +00:00
searchStateStore.setSelectedResultToNext();
2025-10-29 19:35:19 +00:00
}
return;
}
// Only append printable characters to the query
2025-10-29 19:46:20 +00:00
if (inputService.isPrintableKey(e.key, e.ctrlKey, e.metaKey)) {
2025-10-29 19:35:19 +00:00
searchStateStore.appendToQuery(e.key);
userInputService.performSearch();
2025-10-26 09:15:11 +00:00
}
}
2025-11-13 21:15:38 +00:00
function handleSearchResultAcceptance(e?: MouseEvent) {
2025-11-02 20:16:06 +00:00
if ($searchState.selectedResult !== "" && $searchState.position != null && $searchState.trigger != null) {
2025-11-10 12:59:48 +00:00
const node = toNode($searchState.trigger, $searchState.selectedResult);
2025-11-02 20:16:06 +00:00
inputService.deleteTextRange($searchState.position, inputService.getCursorPosition(textareaElement), textareaElement);
inputService.insertElementAtCursor(node, textareaElement);
2025-11-15 11:35:23 +00:00
searchStateStore.resetSearch();
2025-11-02 20:16:06 +00:00
}
2025-11-13 21:15:38 +00:00
e?.preventDefault();
2025-12-13 10:40:20 +00:00
focusInput(true);
2025-11-02 20:16:06 +00:00
}
2025-10-26 09:15:11 +00:00
function handleInput() {
if (textareaElement) {
userRequest = textareaElement.textContent || "";
2025-10-29 19:35:19 +00:00
if (textareaElement.innerHTML !== textareaElement.textContent) {
2025-10-31 15:03:40 +00:00
if (inputService.hasUnauthorizedHTML(textareaElement)) {
inputService.sanitizeToPlainText(textareaElement);
}
2025-10-29 19:35:19 +00:00
}
// If in search mode, synchronize the query with actual text content
if ($searchState.active && $searchState.position !== null) {
const fullText = textareaElement.textContent || "";
const triggerPos = $searchState.position;
2025-12-11 17:11:41 +00:00
// Extract the query portion (from trigger to cursor position)
const currentCursorPos = inputService.getCursorPosition(textareaElement);
const actualQuery = fullText.substring(triggerPos + 1, currentCursorPos);
2025-10-29 19:35:19 +00:00
// Only update if the query has changed
if (actualQuery !== $searchState.query) {
searchStateStore.setQuery(actualQuery);
userInputService.performSearch();
}
}
2025-10-31 15:03:40 +00:00
2025-10-26 09:15:11 +00:00
if (userRequest.trim() === "") {
textareaElement.textContent = "";
}
2026-03-22 20:07:26 +00:00
checkStacked();
2025-10-26 09:15:11 +00:00
}
}
2025-10-29 19:35:19 +00:00
function handleCopy(e: ClipboardEvent) {
e.preventDefault();
const selection = window.getSelection();
2025-10-31 15:03:40 +00:00
2025-10-29 19:35:19 +00:00
if (!selection) {
return;
}
const selectedText = selection.toString();
2025-10-31 15:03:40 +00:00
e.clipboardData?.setData("text/plain", selectedText);
}
2025-12-22 20:02:02 +00:00
async function handleDataTransfer(e: ClipboardEvent | DragEvent) {
2025-10-31 15:03:40 +00:00
e.preventDefault();
2025-12-22 20:02:02 +00:00
e.stopPropagation();
const dataTransfer = e instanceof ClipboardEvent ? e.clipboardData : e.dataTransfer;
2025-10-31 15:03:40 +00:00
2025-12-22 20:02:02 +00:00
const files = await inputService.getFilesFromDataTransfer(dataTransfer);
const plainText = inputService.getTextFromDataTransfer(dataTransfer);
const newAttachments = files.filter(file => !attachments.some(attachment => attachment.base64 === file.base64));
attachments = [...attachments, ...newAttachments];
2025-10-31 15:03:40 +00:00
2025-12-23 12:04:29 +00:00
if (!plainText || plainText.includes("obsidian:")) {
2025-10-31 15:03:40 +00:00
return;
}
inputService.insertTextAtCursor(plainText);
handleInput();
2025-10-29 19:35:19 +00:00
}
2025-12-22 20:02:02 +00:00
function handleDragOver(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
if (e.dataTransfer) {
e.dataTransfer.dropEffect = "copy";
e.dataTransfer.effectAllowed = "copy";
}
}
function handleDragEnter(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
}
function handleDragLeave(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
}
2026-04-19 20:01:24 +00:00
function handleAttachments() {
const input = document.createElement("input");
input.multiple = true;
input.type = "file";
input.onchange = async () => {
if (input.files) {
const dataTransfer = new DataTransfer();
for (let index = 0; index < input.files.length ; index ++) {
dataTransfer.items.add(input.files[index]);
}
const files = await inputService.getFilesFromDataTransfer(dataTransfer);
const newAttachments = files.filter(file => !attachments.some(a => a.base64 === file.base64));
attachments = [...attachments, ...newAttachments];
}
};
input.click();
}
2025-10-29 19:35:19 +00:00
function handleCursorPositionChange() {
if (!$searchState.active || $searchState.position === null) {
return;
}
2025-10-29 19:46:20 +00:00
const currentPosition = inputService.getCursorPosition(textareaElement);
2025-10-29 19:35:19 +00:00
2025-10-29 19:46:20 +00:00
if (!inputService.isInSearchZone(currentPosition, $searchState.position)) {
2025-10-29 19:35:19 +00:00
searchStateStore.resetSearch();
}
2025-10-26 09:15:11 +00:00
}
2025-11-02 23:19:11 +00:00
function handleFocusOut() {
searchStateStore.resetSearch();
}
2025-10-26 09:15:11 +00:00
< / script >
2026-04-19 19:39:04 +00:00
< div id = "input-container" class:stacked >
2026-01-04 18:52:44 +00:00
< div id = "input-display-container" style:padding-top = { attachments . length > 0 ? "var(--size-4-2)" : 0 } >
2026-04-19 19:39:04 +00:00
< InputDisplay bind:this = { inputDisplay } / >
2026-01-04 18:52:44 +00:00
< / div >
2025-12-22 20:02:02 +00:00
< div id = "input-attachments-container" style:padding-top = { attachments . length > 0 ? "var(--size-4-2)" : 0 } >
< ChatAttachments bind:attachments = { attachments } / >
< / div >
2026-01-04 18:52:44 +00:00
< div id = "diff-controls-container" style:padding-top = { inputMode === InputMode . Diff ? "var(--size-4-2)" : 0 } >
< DiffControls diffOpen = { inputMode === InputMode . Diff } / >
2025-11-27 12:39:08 +00:00
< / div >
2025-10-29 21:33:45 +00:00
< div id = "input-search-results-container" style:padding-top = { $searchState . results . length > 0 ? "var(--size-4-2)" : 0 } >
2025-11-02 20:16:06 +00:00
< ChatSearchResults searchState = { $searchState } onResultAccept= { handleSearchResultAcceptance } / >
< / div >
2026-03-01 12:37:18 +00:00
< div id = "user-instruction-container" style:padding-top = { userInstructionAreaActive ? "var(--size-4-2)" : 0 } >
< UserInstruction focusInput = { focusInput } bind:userInstructionAreaActive= { userInstructionAreaActive } / >
2025-10-29 19:35:19 +00:00
< / div >
2026-04-19 19:39:04 +00:00
< div id = "chat-mode-selector-container" style:padding-top = { chatModeSelectionAreaActive ? "var(--size-4-2)" : 0 } >
< ChatModeSelector focusInput = { focusInput } bind:chatModeSelectionAreaActive= { chatModeSelectionAreaActive } bind:currentChatMode = { chatMode } / >
< / div >
2025-11-02 20:16:06 +00:00
< button
id="user-instruction-button"
2026-04-19 19:39:04 +00:00
class:input-button-highlight={ userInstructionActive }
2025-11-02 20:16:06 +00:00
bind:this={ userInstructionButton }
2026-03-01 12:37:18 +00:00
on:click={ toggleUserInstructionArea }
2026-04-10 09:47:23 +00:00
aria-label={ Copy . ButtonUserInstruction } >
< / button >
< button
id="web-search-button"
2026-04-19 19:39:04 +00:00
class:input-button-highlight={ settingsService . settings . enableWebSearch }
2026-04-10 09:47:23 +00:00
bind:this={ webSearchButton }
on:click={ toggleWebSearch }
aria-label={ settingsService . settings . enableWebSearch ? Copy.ButtonTurnOffWebSearch : Copy.ButtonTurnOnWebSearch } >
2025-11-02 20:16:06 +00:00
< / button >
2025-10-26 09:15:11 +00:00
< div
id="input-field"
class:error={ hasNoApiKey }
bind:this={ textareaElement }
2025-10-31 15:03:40 +00:00
contenteditable="plaintext-only"
2025-10-26 09:15:11 +00:00
on:keydown={ handleKeydown }
2025-11-12 22:05:12 +00:00
on:beforeinput={ handleBeforeInput }
2025-10-26 09:15:11 +00:00
on:input={ handleInput }
2025-10-29 19:35:19 +00:00
on:copy={ handleCopy }
2025-12-22 20:02:02 +00:00
on:paste={ handleDataTransfer }
on:drop={ handleDataTransfer }
on:dragover={ handleDragOver }
on:dragenter={ handleDragEnter }
on:dragleave={ handleDragLeave }
2025-10-29 19:35:19 +00:00
on:click={ handleCursorPositionChange }
on:keyup={ handleCursorPositionChange }
2025-11-02 23:19:11 +00:00
on:focusout={ handleFocusOut }
2026-01-04 18:52:44 +00:00
data-placeholder={ inputPlaceholder }
2025-10-26 09:15:11 +00:00
role="textbox"
aria-multiline="true"
tabindex="0">
< / div >
< button
2026-04-19 19:39:04 +00:00
id="chat-attachment-button"
bind:this={ attachmentButton }
2026-04-23 16:12:06 +00:00
on:click={ handleAttachments }
2025-10-26 09:15:11 +00:00
disabled={ isSubmitting }
2026-04-19 20:01:24 +00:00
aria-label={ Copy . ButtonAttachFiles } >
2026-01-04 18:52:44 +00:00
< / button >
< button
2026-04-19 19:39:04 +00:00
id="chat-mode-button"
class:input-button-highlight={ chatModeAllowsEdits ( chatMode )}
bind:this={ chatModeButton }
2026-04-23 16:12:06 +00:00
on:click={ toggleChatModeSelectionArea }
2026-01-04 18:52:44 +00:00
disabled={ isSubmitting }
2026-04-19 19:39:04 +00:00
aria-label={ Copy . ButtonChangeChatMode } >
2025-10-26 09:15:11 +00:00
< / button >
< button
id="submit-button"
bind:this={ submitButton }
2025-12-05 23:34:52 +00:00
on:click={() => {
2026-01-04 18:52:44 +00:00
if (inputMode === InputMode.Question) {
userRequest.trim() === "" ? handleStop() : handleAnswer();
} else if (inputMode === InputMode.Diff) {
2025-12-05 23:34:52 +00:00
userRequest.trim() === "" ? handleStop() : handleSuggestion();
} else {
isSubmitting ? handleStop() : handleSubmit();
}
}}
2026-01-04 18:52:44 +00:00
disabled={ submitDisabled }
aria-label={ submitAriaLabel } >
2025-10-26 09:15:11 +00:00
< / button >
< / div >
< style >
#input-container {
2026-01-03 11:15:32 +00:00
grid-row: 3;
2025-10-26 09:15:11 +00:00
grid-column: 1;
display: grid;
2026-04-23 16:12:06 +00:00
grid-template-rows: auto auto auto auto auto var(--size-4-3) 1fr var(--size-4-3);
2026-04-10 09:47:23 +00:00
grid-template-columns: var(--size-4-3) auto var(--size-4-2) auto var(--size-4-2) 1fr var(--size-4-2) auto var(--size-4-2) auto var(--size-4-2) auto var(--size-4-3);
2026-01-29 14:22:44 +00:00
border-radius: var(--radius-l);
2025-10-26 09:15:11 +00:00
background-color: var(--background-primary);
}
2026-01-04 18:52:44 +00:00
#input-display-container {
2025-10-29 19:35:19 +00:00
grid-row: 1;
2026-04-10 09:47:23 +00:00
grid-column: 2 / 13;
2025-10-29 19:35:19 +00:00
}
2026-01-04 18:52:44 +00:00
#input-attachments-container {
2025-11-27 12:39:08 +00:00
grid-row: 2;
2026-04-10 09:47:23 +00:00
grid-column: 2 / 13;
2025-11-27 12:39:08 +00:00
}
2026-01-04 18:52:44 +00:00
#diff-controls-container {
2025-12-22 20:02:02 +00:00
grid-row: 3;
2026-04-10 09:47:23 +00:00
grid-column: 2 / 13;
2026-01-04 18:52:44 +00:00
}
#input-search-results-container {
grid-row: 4;
2026-04-10 09:47:23 +00:00
grid-column: 2 / 13;
2025-12-22 20:02:02 +00:00
}
2025-11-02 20:16:06 +00:00
#user-instruction-container {
2026-01-04 18:52:44 +00:00
grid-row: 4;
2026-04-10 09:47:23 +00:00
grid-column: 2 / 13;
2025-11-02 20:16:06 +00:00
}
2026-04-19 19:39:04 +00:00
#chat-mode-selector-container {
2026-04-23 16:12:06 +00:00
grid-row: 5;
2026-04-19 19:39:04 +00:00
grid-column: 2 / 13;
}
2025-11-02 20:16:06 +00:00
#user-instruction-button {
2026-04-23 16:12:06 +00:00
grid-row: 7;
2025-10-26 09:15:11 +00:00
grid-column: 2;
2026-04-19 19:39:04 +00:00
border-radius: var(--radius-xl);
padding: var(--size-4-2);
2025-11-02 20:16:06 +00:00
align-self: end;
transition-duration: 0.5s;
}
2025-11-04 22:51:22 +00:00
:global(.is-mobile) #user-instruction-button {
max-height: 2rem;
}
2026-04-10 09:47:23 +00:00
#web-search-button {
2026-04-23 16:12:06 +00:00
grid-row: 7;
2025-11-02 20:16:06 +00:00
grid-column: 4;
2026-04-19 19:39:04 +00:00
border-radius: var(--radius-xl);
padding: var(--size-4-2);
2026-04-10 09:47:23 +00:00
align-self: end;
transition-duration: 0.5s;
}
:global(.is-mobile) #web-search-button {
max-height: 2rem;
}
#input-field {
2026-04-23 16:12:06 +00:00
grid-row: 7;
2026-04-10 09:47:23 +00:00
grid-column: 6;
2025-10-26 09:15:11 +00:00
height: 100%;
max-height: 30vh;
2026-01-29 14:22:44 +00:00
border-radius: var(--radius-m);
2025-10-26 09:15:11 +00:00
font-weight: var(--input-font-weight);
border-width: var(--input-border-width);
border-style: solid;
border-color: var(--background-modifier-border);
padding: var(--size-2-2) var(--size-2-3);
background-color: var(--background-primary);
font-family: var(--font-interface-theme);
resize: none;
overflow-y: auto;
overflow-x: hidden;
scroll-behavior: smooth;
color: var(--font-interface-theme);
transition: border-color 0.5s ease-out;
word-wrap: break-word;
white-space: pre-wrap;
}
2025-11-04 22:51:22 +00:00
:global(.is-mobile) #input-field {
align-content: end;
}
2025-10-26 09:15:11 +00:00
#input-field:focus {
border-color: var(--color-accent);
box-shadow: 0px 0px 4px 1px var(--color-accent);
transition: border-color 0.5s ease-out;
}
#input-field.error,
#input-field.error:focus {
border-color: var(--color-red);
box-shadow: 0px 0px 4px 1px var(--color-red);
transition: border-color 0.5s ease-out;
}
#input-field::-webkit-scrollbar {
display: none;
}
#input-field:empty::before {
content: attr(data-placeholder);
color: var(--text-muted);
opacity: 0.75;
pointer-events: none;
}
#input-field[contenteditable]:focus {
outline: none;
}
2026-04-19 19:39:04 +00:00
#chat-attachment-button {
2026-04-23 16:12:06 +00:00
grid-row: 7;
2026-04-10 09:47:23 +00:00
grid-column: 8;
2026-04-19 19:39:04 +00:00
border-radius: var(--radius-xl);
padding: var(--size-4-2);
2025-10-26 09:15:11 +00:00
align-self: end;
transition-duration: 0.5s;
}
2026-04-19 19:39:04 +00:00
:global(.is-mobile) #chat-attachment-button {
2025-11-04 22:51:22 +00:00
max-height: 2rem;
}
2026-04-19 19:39:04 +00:00
#chat-mode-button {
2026-04-23 16:12:06 +00:00
grid-row: 7;
2026-04-10 09:47:23 +00:00
grid-column: 10;
2026-04-19 19:39:04 +00:00
border-radius: var(--radius-xl);
padding: var(--size-4-2);
2026-01-04 18:52:44 +00:00
align-self: end;
transition-duration: 0.5s;
}
2026-04-19 19:39:04 +00:00
:global(.is-mobile) #chat-mode-button {
max-height: 2rem;
2026-01-04 18:52:44 +00:00
}
2026-04-19 19:39:04 +00:00
.input-button-highlight {
box-shadow: 0px 0px 2px 1px var(--color-accent);
2026-01-04 18:52:44 +00:00
}
#submit-button {
2026-04-23 16:12:06 +00:00
grid-row: 7;
2026-04-10 09:47:23 +00:00
grid-column: 12;
2026-04-19 19:39:04 +00:00
border-radius: var(--radius-xl);
padding-left: var(--size-4-2);
padding-right: var(--size-4-2);
2025-10-26 09:15:11 +00:00
align-self: end;
transition-duration: 0.5s;
background-color: var(--interactive-accent);
}
2025-11-04 22:51:22 +00:00
:global(.is-mobile) #submit-button {
max-height: 2rem;
}
2025-10-26 09:15:11 +00:00
#submit-button:not(:disabled):hover {
cursor: pointer;
background-color: var(--interactive-accent-hover);
}
2026-03-22 20:07:26 +00:00
/* Stacked layout: input above, buttons below (desktop only, when content wraps) */
#input-container.stacked {
2026-04-23 16:12:06 +00:00
grid-template-rows: auto auto auto auto auto var(--size-4-3) 1fr var(--size-4-2) auto var(--size-4-3);
2026-03-22 20:07:26 +00:00
}
#input-container.stacked #input-field {
2026-04-10 09:47:23 +00:00
grid-column: 2 / 13;
2026-03-22 20:07:26 +00:00
}
#input-container.stacked #user-instruction-button {
2026-04-23 16:12:06 +00:00
grid-row: 9;
2026-03-22 20:07:26 +00:00
}
2026-04-10 09:47:23 +00:00
#input-container.stacked #web-search-button {
2026-04-23 16:12:06 +00:00
grid-row: 9;
2026-04-10 09:47:23 +00:00
}
2026-04-19 19:39:04 +00:00
#input-container.stacked #chat-attachment-button {
2026-04-23 16:12:06 +00:00
grid-row: 9;
2026-03-22 20:07:26 +00:00
}
2026-04-19 19:39:04 +00:00
#input-container.stacked #chat-mode-button {
2026-04-23 16:12:06 +00:00
grid-row: 9;
2026-03-22 20:07:26 +00:00
}
#input-container.stacked #submit-button {
2026-04-23 16:12:06 +00:00
grid-row: 9;
2026-03-22 20:07:26 +00:00
}
2026-01-05 21:49:51 +00:00
/* Narrow/mobile layout: input above, buttons below */
:global(.is-mobile) #input-container {
2026-04-23 16:12:06 +00:00
grid-template-rows: auto auto auto auto auto var(--size-4-3) 1fr var(--size-4-2) auto var(--size-4-3);
2026-04-10 09:47:23 +00:00
grid-template-columns: var(--size-4-3) auto var(--size-4-2) auto 1fr auto var(--size-4-2) auto var(--size-4-2) auto var(--size-4-3);
2026-01-05 21:49:51 +00:00
}
:global(.is-mobile) #input-display-container,
:global(.is-mobile) #input-attachments-container,
:global(.is-mobile) #diff-controls-container,
:global(.is-mobile) #input-search-results-container,
2026-04-19 19:39:04 +00:00
:global(.is-mobile) #user-instruction-container,
:global(.is-mobile) #chat-mode-selector-container {
2026-04-10 09:47:23 +00:00
grid-column: 2 / 11;
2026-01-05 21:49:51 +00:00
}
:global(.is-mobile) #input-field {
2026-04-23 16:12:06 +00:00
grid-row: 7;
2026-04-10 09:47:23 +00:00
grid-column: 2 / 11;
2026-01-05 21:49:51 +00:00
}
:global(.is-mobile) #user-instruction-button {
2026-04-23 16:12:06 +00:00
grid-row: 9;
2026-01-05 21:49:51 +00:00
grid-column: 2;
}
2026-04-10 09:47:23 +00:00
:global(.is-mobile) #web-search-button {
2026-04-23 16:12:06 +00:00
grid-row: 9;
2026-01-05 21:49:51 +00:00
grid-column: 4;
}
2026-04-19 19:39:04 +00:00
:global(.is-mobile) #chat-attachment-button {
2026-04-23 16:12:06 +00:00
grid-row: 9;
2026-01-05 21:49:51 +00:00
grid-column: 6;
}
2026-04-19 19:39:04 +00:00
:global(.is-mobile) #chat-mode-button {
2026-04-23 16:12:06 +00:00
grid-row: 9;
2026-01-05 21:49:51 +00:00
grid-column: 8;
}
2026-04-10 09:47:23 +00:00
:global(.is-mobile) #submit-button {
2026-04-23 16:12:06 +00:00
grid-row: 9;
2026-04-10 09:47:23 +00:00
grid-column: 10;
}
2025-10-26 09:15:11 +00:00
< / style >