From f194a784b00b2e21067cc15bced152cfec7bb810 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 22 Oct 2025 17:33:08 -0500 Subject: [PATCH] wip: desktop work --- .../src/components/prompt-form-helpers.ts | 164 ----- .../src/components/prompt-form-hooks.ts | 396 ------------ .../desktop/src/components/prompt-form.tsx | 581 ------------------ 3 files changed, 1141 deletions(-) delete mode 100644 packages/desktop/src/components/prompt-form-helpers.ts delete mode 100644 packages/desktop/src/components/prompt-form-hooks.ts delete mode 100644 packages/desktop/src/components/prompt-form.tsx diff --git a/packages/desktop/src/components/prompt-form-helpers.ts b/packages/desktop/src/components/prompt-form-helpers.ts deleted file mode 100644 index 298b831e..00000000 --- a/packages/desktop/src/components/prompt-form-helpers.ts +++ /dev/null @@ -1,164 +0,0 @@ -import type { TextSelection } from "@/context/local" -import { getFilename } from "@/utils" - -export interface PromptTextPart { - kind: "text" - value: string -} - -export interface PromptAttachmentPart { - kind: "file" - token: string - display: string - path: string - selection?: TextSelection - origin: "context" | "active" -} - -export interface PromptInterimPart { - kind: "interim" - value: string - leadingSpace: boolean -} - -export type PromptContentPart = PromptTextPart | PromptAttachmentPart - -export type PromptDisplaySegment = - | { kind: "text"; value: string } - | { kind: "attachment"; part: PromptAttachmentPart; source: string } - | PromptInterimPart - -export interface AttachmentCandidate { - origin: "context" | "active" - path: string - selection?: TextSelection - display: string -} - -export interface PromptSubmitValue { - text: string - parts: PromptContentPart[] -} - -export const mentionPattern = /@([A-Za-z0-9_\-./]+)/g -export const mentionTriggerPattern = /(^|\s)@([A-Za-z0-9_\-./]*)$/ - -export type PromptSegment = (PromptTextPart | PromptAttachmentPart) & { - start: number - end: number -} - -export type PromptAttachmentSegment = PromptAttachmentPart & { - start: number - end: number -} - -function pushTextPart(parts: PromptContentPart[], value: string) { - if (!value) return - const last = parts[parts.length - 1] - if (last && last.kind === "text") { - last.value += value - return - } - parts.push({ kind: "text", value }) -} - -function addTextSegment(segments: PromptSegment[], start: number, value: string) { - if (!value) return - segments.push({ kind: "text", value, start, end: start + value.length }) -} - -export function createAttachmentDisplay(path: string, selection?: TextSelection) { - const base = getFilename(path) - if (!selection) return base - return `${base} (${selection.startLine}-${selection.endLine})` -} - -export function registerCandidate( - map: Map, - candidate: AttachmentCandidate, - tokens: (string | undefined)[], -) { - for (const token of tokens) { - if (!token) continue - const normalized = token.toLowerCase() - if (map.has(normalized)) continue - map.set(normalized, candidate) - } -} - -export function parsePrompt(value: string, lookup: Map) { - const segments: PromptSegment[] = [] - if (!value) return { parts: [] as PromptContentPart[], segments } - - const pushTextRange = (rangeStart: number, rangeEnd: number) => { - if (rangeEnd <= rangeStart) return - const text = value.slice(rangeStart, rangeEnd) - let cursor = 0 - for (const match of text.matchAll(mentionPattern)) { - const localIndex = match.index ?? 0 - if (localIndex > cursor) { - addTextSegment(segments, rangeStart + cursor, text.slice(cursor, localIndex)) - } - const token = match[1] - const candidate = lookup.get(token.toLowerCase()) - if (candidate) { - const start = rangeStart + localIndex - const end = start + match[0].length - segments.push({ - kind: "file", - token, - display: candidate.display, - path: candidate.path, - selection: candidate.selection, - origin: candidate.origin, - start, - end, - }) - } else { - addTextSegment(segments, rangeStart + localIndex, match[0]) - } - cursor = localIndex + match[0].length - } - if (cursor < text.length) { - addTextSegment(segments, rangeStart + cursor, text.slice(cursor)) - } - } - - pushTextRange(0, value.length) - - const parts: PromptContentPart[] = [] - for (const segment of segments) { - if (segment.kind === "text") { - pushTextPart(parts, segment.value) - } else { - const { start, end, ...attachment } = segment - parts.push(attachment as PromptAttachmentPart) - } - } - return { parts, segments } -} - -export function composeDisplaySegments( - segments: PromptSegment[], - inputValue: string, - interim: string, -): PromptDisplaySegment[] { - if (segments.length === 0 && !interim) return [] - - const display: PromptDisplaySegment[] = segments.map((segment) => { - if (segment.kind === "text") { - return { kind: "text", value: segment.value } - } - const { start, end, ...part } = segment - const placeholder = inputValue.slice(start, end) - return { kind: "file", part: part as PromptAttachmentPart, source: placeholder } - }) - - if (interim) { - const leadingSpace = !!(inputValue && !inputValue.endsWith(" ") && !interim.startsWith(" ")) - display.push({ kind: "interim", value: interim, leadingSpace }) - } - - return display -} diff --git a/packages/desktop/src/components/prompt-form-hooks.ts b/packages/desktop/src/components/prompt-form-hooks.ts deleted file mode 100644 index c4d18fa7..00000000 --- a/packages/desktop/src/components/prompt-form-hooks.ts +++ /dev/null @@ -1,396 +0,0 @@ -import { createEffect, createMemo, createResource, type Accessor } from "solid-js" -import type { SetStoreFunction } from "solid-js/store" -import { getDirectory, getFilename } from "@/utils" -import { createSpeechRecognition } from "@/utils/speech" -import { - createAttachmentDisplay, - mentionPattern, - mentionTriggerPattern, - type PromptAttachmentPart, - type PromptAttachmentSegment, -} from "./prompt-form-helpers" -import type { LocalFile, TextSelection } from "@/context/local" - -export type MentionRange = { - start: number - end: number -} - -export interface PromptFormState { - promptInput: string - isDragOver: boolean - mentionOpen: boolean - mentionQuery: string - mentionRange: MentionRange | undefined - mentionIndex: number - mentionAnchorOffset: { x: number; y: number } - inlineAliases: Map -} - -interface MentionControllerOptions { - state: PromptFormState - setState: SetStoreFunction - attachmentSegments: Accessor - getInputRef: () => HTMLTextAreaElement | undefined - getOverlayRef: () => HTMLDivElement | undefined - getMeasureRef: () => HTMLDivElement | undefined - searchFiles: (query: string) => Promise - resolveFile: (path: string) => LocalFile | undefined - addContextFile: (path: string, selection?: TextSelection) => void - getActiveContext: () => { path: string; selection?: TextSelection } | undefined -} - -interface MentionKeyDownOptions { - event: KeyboardEvent & { currentTarget: HTMLTextAreaElement } - mentionItems: () => string[] - insertMention: (path: string) => void -} - -interface ScrollSyncOptions { - state: PromptFormState - getInputRef: () => HTMLTextAreaElement | undefined - getOverlayRef: () => HTMLDivElement | undefined - interim: Accessor - updateMentionPosition: (element: HTMLTextAreaElement, range?: MentionRange) => void -} - -export function usePromptSpeech(updatePromptInput: (updater: (prev: string) => string) => void) { - return createSpeechRecognition({ - onFinal: (text) => updatePromptInput((prev) => (prev && !prev.endsWith(" ") ? `${prev} ` : prev) + text), - }) -} - -export function useMentionController(options: MentionControllerOptions) { - const mentionSource = createMemo(() => (options.state.mentionOpen ? options.state.mentionQuery : undefined)) - const [mentionResults, { mutate: mutateMentionResults }] = createResource(mentionSource, (query) => { - if (!options.state.mentionOpen) return [] - return options.searchFiles(query ?? "") - }) - const mentionItems = createMemo(() => mentionResults() ?? []) - - createEffect(() => { - if (!options.state.mentionOpen) return - options.state.mentionQuery - options.setState("mentionIndex", 0) - }) - - createEffect(() => { - if (!options.state.mentionOpen) return - queueMicrotask(() => { - const input = options.getInputRef() - if (!input) return - if (document.activeElement === input) return - input.focus() - }) - }) - - createEffect(() => { - const used = new Set() - for (const match of options.state.promptInput.matchAll(mentionPattern)) { - const token = match[1] - if (token) used.add(token.toLowerCase()) - } - options.setState("inlineAliases", (prev) => { - if (prev.size === 0) return prev - const next = new Map(prev) - let changed = false - for (const key of prev.keys()) { - if (!used.has(key.toLowerCase())) { - next.delete(key) - changed = true - } - } - return changed ? next : prev - }) - }) - - createEffect(() => { - if (!options.state.mentionOpen) return - const items = mentionItems() - if (items.length === 0) { - options.setState("mentionIndex", 0) - return - } - if (options.state.mentionIndex < items.length) return - options.setState("mentionIndex", items.length - 1) - }) - - createEffect(() => { - if (!options.state.mentionOpen) return - const rangeValue = options.state.mentionRange - if (!rangeValue) return - options.state.promptInput - queueMicrotask(() => { - const input = options.getInputRef() - if (!input) return - updateMentionPosition(input, rangeValue) - }) - }) - - function closeMention() { - if (options.state.mentionOpen) options.setState("mentionOpen", false) - options.setState("mentionQuery", "") - options.setState("mentionRange", undefined) - options.setState("mentionIndex", 0) - mutateMentionResults(() => undefined) - options.setState("mentionAnchorOffset", { x: 0, y: 0 }) - } - - function updateMentionPosition(element: HTMLTextAreaElement, rangeValue = options.state.mentionRange) { - const measure = options.getMeasureRef() - if (!measure) return - if (!rangeValue) return - measure.style.width = `${element.clientWidth}px` - const measurement = element.value.slice(0, rangeValue.end) - measure.textContent = measurement - const caretSpan = document.createElement("span") - caretSpan.textContent = "\u200b" - measure.append(caretSpan) - const caretRect = caretSpan.getBoundingClientRect() - const containerRect = measure.getBoundingClientRect() - measure.removeChild(caretSpan) - const left = caretRect.left - containerRect.left - const top = caretRect.top - containerRect.top - element.scrollTop - options.setState("mentionAnchorOffset", { x: left, y: top < 0 ? 0 : top }) - } - - function isValidMentionQuery(value: string) { - return /^[A-Za-z0-9_\-./]*$/.test(value) - } - - function syncMentionFromCaret(element: HTMLTextAreaElement) { - if (!options.state.mentionOpen) return - const rangeValue = options.state.mentionRange - if (!rangeValue) { - closeMention() - return - } - const caret = element.selectionEnd ?? element.selectionStart ?? element.value.length - if (rangeValue.start < 0 || rangeValue.start >= element.value.length) { - closeMention() - return - } - if (element.value[rangeValue.start] !== "@") { - closeMention() - return - } - if (caret <= rangeValue.start) { - closeMention() - return - } - const mentionValue = element.value.slice(rangeValue.start + 1, caret) - if (!isValidMentionQuery(mentionValue)) { - closeMention() - return - } - options.setState("mentionRange", { start: rangeValue.start, end: caret }) - options.setState("mentionQuery", mentionValue) - updateMentionPosition(element, { start: rangeValue.start, end: caret }) - } - - function tryOpenMentionFromCaret(element: HTMLTextAreaElement) { - const selectionStart = element.selectionStart ?? element.value.length - const selectionEnd = element.selectionEnd ?? selectionStart - if (selectionStart !== selectionEnd) return false - const caret = selectionEnd - if (options.attachmentSegments().some((segment) => caret >= segment.start && caret <= segment.end)) { - return false - } - const before = element.value.slice(0, caret) - const match = before.match(mentionTriggerPattern) - if (!match) return false - const token = match[2] ?? "" - const start = caret - token.length - 1 - if (start < 0) return false - options.setState("mentionOpen", true) - options.setState("mentionRange", { start, end: caret }) - options.setState("mentionQuery", token) - options.setState("mentionIndex", 0) - queueMicrotask(() => { - updateMentionPosition(element, { start, end: caret }) - }) - return true - } - - function handlePromptInput(event: InputEvent & { currentTarget: HTMLTextAreaElement }) { - const element = event.currentTarget - options.setState("promptInput", element.value) - if (options.state.mentionOpen) { - syncMentionFromCaret(element) - if (options.state.mentionOpen) return - } - const isDeletion = event.inputType ? event.inputType.startsWith("delete") : false - if (!isDeletion && tryOpenMentionFromCaret(element)) return - closeMention() - } - - function handleMentionKeyDown({ event, mentionItems: items, insertMention }: MentionKeyDownOptions) { - if (!options.state.mentionOpen) return false - const list = items() - if (event.key === "ArrowDown") { - event.preventDefault() - if (list.length === 0) return true - const next = options.state.mentionIndex + 1 >= list.length ? 0 : options.state.mentionIndex + 1 - options.setState("mentionIndex", next) - return true - } - if (event.key === "ArrowUp") { - event.preventDefault() - if (list.length === 0) return true - const previous = options.state.mentionIndex - 1 < 0 ? list.length - 1 : options.state.mentionIndex - 1 - options.setState("mentionIndex", previous) - return true - } - if (event.key === "Enter") { - event.preventDefault() - const targetItem = list[options.state.mentionIndex] ?? list[0] - if (targetItem) insertMention(targetItem) - return true - } - if (event.key === "Escape") { - event.preventDefault() - closeMention() - return true - } - return false - } - - function generateMentionAlias(path: string) { - const existing = new Set() - for (const key of options.state.inlineAliases.keys()) { - existing.add(key.toLowerCase()) - } - for (const match of options.state.promptInput.matchAll(mentionPattern)) { - const token = match[1] - if (token) existing.add(token.toLowerCase()) - } - - const base = getFilename(path) - if (base) { - if (!existing.has(base.toLowerCase())) return base - } - - const directory = getDirectory(path) - if (base && directory) { - const segments = directory.split("/").filter(Boolean) - for (let i = segments.length - 1; i >= 0; i -= 1) { - const candidate = `${segments.slice(i).join("/")}/${base}` - if (!existing.has(candidate.toLowerCase())) return candidate - } - } - - if (!existing.has(path.toLowerCase())) return path - - const fallback = base || path || "file" - let index = 2 - let candidate = `${fallback}-${index}` - while (existing.has(candidate.toLowerCase())) { - index += 1 - candidate = `${fallback}-${index}` - } - return candidate - } - - function insertMention(path: string) { - const input = options.getInputRef() - if (!input) return - const rangeValue = options.state.mentionRange - if (!rangeValue) return - const node = options.resolveFile(path) - const alias = generateMentionAlias(path) - const mentionText = `@${alias}` - const value = options.state.promptInput - const before = value.slice(0, rangeValue.start) - const after = value.slice(rangeValue.end) - const needsLeadingSpace = before.length > 0 && !/\s$/.test(before) - const needsTrailingSpace = after.length > 0 && !/^\s/.test(after) - const leading = needsLeadingSpace ? `${before} ` : before - const trailingSpacer = needsTrailingSpace ? " " : "" - const nextValue = `${leading}${mentionText}${trailingSpacer}${after}` - const origin = options.getActiveContext()?.path === path ? "active" : "context" - const part: PromptAttachmentPart = { - kind: "file", - token: alias, - display: createAttachmentDisplay(path, node?.selection), - path, - selection: node?.selection, - origin, - } - options.setState("promptInput", nextValue) - if (input.value !== nextValue) { - input.value = nextValue - } - options.setState("inlineAliases", (prev) => { - const next = new Map(prev) - next.set(alias, part) - return next - }) - options.addContextFile(path, node?.selection) - closeMention() - queueMicrotask(() => { - const caret = leading.length + mentionText.length + trailingSpacer.length - input.setSelectionRange(caret, caret) - syncMentionFromCaret(input) - }) - } - - return { - mentionResults, - mentionItems, - closeMention, - syncMentionFromCaret, - tryOpenMentionFromCaret, - updateMentionPosition, - handlePromptInput, - handleMentionKeyDown, - insertMention, - } -} - -export function usePromptScrollSync(options: ScrollSyncOptions) { - let shouldAutoScroll = true - - createEffect(() => { - options.state.promptInput - options.interim() - queueMicrotask(() => { - const input = options.getInputRef() - const overlay = options.getOverlayRef() - if (!input || !overlay) return - if (!shouldAutoScroll) { - overlay.scrollTop = input.scrollTop - if (options.state.mentionOpen) options.updateMentionPosition(input) - return - } - const maxInputScroll = input.scrollHeight - input.clientHeight - const next = maxInputScroll > 0 ? maxInputScroll : 0 - input.scrollTop = next - overlay.scrollTop = next - if (options.state.mentionOpen) options.updateMentionPosition(input) - }) - }) - - function handlePromptScroll(event: Event & { currentTarget: HTMLTextAreaElement }) { - const target = event.currentTarget - shouldAutoScroll = target.scrollTop + target.clientHeight >= target.scrollHeight - 4 - const overlay = options.getOverlayRef() - if (overlay) overlay.scrollTop = target.scrollTop - if (options.state.mentionOpen) options.updateMentionPosition(target) - } - - function resetScrollPosition() { - shouldAutoScroll = true - const input = options.getInputRef() - const overlay = options.getOverlayRef() - if (input) input.scrollTop = 0 - if (overlay) overlay.scrollTop = 0 - } - - return { - handlePromptScroll, - resetScrollPosition, - setAutoScroll: (value: boolean) => { - shouldAutoScroll = value - }, - } -} diff --git a/packages/desktop/src/components/prompt-form.tsx b/packages/desktop/src/components/prompt-form.tsx deleted file mode 100644 index 06fbfbb0..00000000 --- a/packages/desktop/src/components/prompt-form.tsx +++ /dev/null @@ -1,581 +0,0 @@ -import { For, Show, createMemo, onCleanup, type JSX } from "solid-js" -import { createStore } from "solid-js/store" -import { Popover } from "@kobalte/core/popover" -import { Tooltip, Button, Icon, Select } from "@opencode-ai/ui" -import { FileIcon, IconButton } from "@/ui" -import { useLocal } from "@/context" -import type { FileContext, LocalFile } from "@/context/local" -import { getDirectory, getFilename } from "@/utils" -import { composeDisplaySegments, createAttachmentDisplay, parsePrompt, registerCandidate } from "./prompt-form-helpers" -import type { - AttachmentCandidate, - PromptAttachmentPart, - PromptAttachmentSegment, - PromptDisplaySegment, - PromptSubmitValue, -} from "./prompt-form-helpers" -import { useMentionController, usePromptScrollSync, usePromptSpeech, type PromptFormState } from "./prompt-form-hooks" - -interface PromptFormProps { - class?: string - classList?: Record - onSubmit: (prompt: PromptSubmitValue) => Promise | void - onOpenModelSelect: () => void - onInputRefChange?: (element: HTMLTextAreaElement | undefined) => void -} - -export default function PromptForm(props: PromptFormProps) { - const local = useLocal() - - const [state, setState] = createStore({ - promptInput: "", - isDragOver: false, - mentionOpen: false, - mentionQuery: "", - mentionRange: undefined, - mentionIndex: 0, - mentionAnchorOffset: { x: 0, y: 0 }, - inlineAliases: new Map(), - }) - - const placeholderText = "Start typing or speaking..." - - const { - isSupported, - isRecording, - interim: interimTranscript, - start: startSpeech, - stop: stopSpeech, - } = usePromptSpeech((updater) => setState("promptInput", updater)) - - let inputRef: HTMLTextAreaElement | undefined = undefined - let overlayContainerRef: HTMLDivElement | undefined = undefined - let mentionMeasureRef: HTMLDivElement | undefined = undefined - - const attachmentLookup = createMemo(() => { - const map = new Map() - const activeFile = local.context.active() - if (activeFile) { - registerCandidate( - map, - { - origin: "active", - path: activeFile.path, - selection: activeFile.selection, - display: createAttachmentDisplay(activeFile.path, activeFile.selection), - }, - [activeFile.path, getFilename(activeFile.path)], - ) - } - for (const item of local.context.all()) { - registerCandidate( - map, - { - origin: "context", - path: item.path, - selection: item.selection, - display: createAttachmentDisplay(item.path, item.selection), - }, - [item.path, getFilename(item.path)], - ) - } - for (const [alias, part] of state.inlineAliases) { - registerCandidate( - map, - { - origin: part.origin, - path: part.path, - selection: part.selection, - display: part.display ?? createAttachmentDisplay(part.path, part.selection), - }, - [alias], - ) - } - return map - }) - - const parsedPrompt = createMemo(() => parsePrompt(state.promptInput, attachmentLookup())) - const baseParts = createMemo(() => parsedPrompt().parts) - const attachmentSegments = createMemo(() => - parsedPrompt().segments.filter((segment): segment is PromptAttachmentSegment => segment.kind === "attachment"), - ) - - const { - mentionResults, - mentionItems, - closeMention, - syncMentionFromCaret, - updateMentionPosition, - handlePromptInput, - handleMentionKeyDown, - insertMention, - } = useMentionController({ - state, - setState, - attachmentSegments, - getInputRef: () => inputRef, - getOverlayRef: () => overlayContainerRef, - getMeasureRef: () => mentionMeasureRef, - searchFiles: (query) => local.file.search(query), - resolveFile: (path) => local.file.node(path) ?? undefined, - addContextFile: (path, selection) => - local.context.add({ - type: "file", - path, - selection, - }), - getActiveContext: () => local.context.active() ?? undefined, - }) - - const { handlePromptScroll, resetScrollPosition } = usePromptScrollSync({ - state, - getInputRef: () => inputRef, - getOverlayRef: () => overlayContainerRef, - interim: () => (isRecording() ? interimTranscript() : ""), - updateMentionPosition, - }) - - const displaySegments = createMemo(() => { - const value = state.promptInput - const segments = parsedPrompt().segments - const interim = isRecording() ? interimTranscript() : "" - return composeDisplaySegments(segments, value, interim) - }) - - const hasDisplaySegments = createMemo(() => displaySegments().length > 0) - - function handleAttachmentNavigation( - event: KeyboardEvent & { currentTarget: HTMLTextAreaElement }, - direction: "left" | "right", - ) { - const element = event.currentTarget - const caret = element.selectionStart ?? 0 - const segments = attachmentSegments() - if (direction === "left") { - let match = segments.find((segment) => caret > segment.start && caret <= segment.end) - if (!match && element.selectionStart !== element.selectionEnd) { - match = segments.find( - (segment) => element.selectionStart === segment.start && element.selectionEnd === segment.end, - ) - } - if (!match) return false - event.preventDefault() - if (element.selectionStart === match.start && element.selectionEnd === match.end) { - const next = Math.max(0, match.start) - element.setSelectionRange(next, next) - syncMentionFromCaret(element) - return true - } - element.setSelectionRange(match.start, match.end) - syncMentionFromCaret(element) - return true - } - if (direction === "right") { - let match = segments.find((segment) => caret >= segment.start && caret < segment.end) - if (!match && element.selectionStart !== element.selectionEnd) { - match = segments.find( - (segment) => element.selectionStart === segment.start && element.selectionEnd === segment.end, - ) - } - if (!match) return false - event.preventDefault() - if (element.selectionStart === match.start && element.selectionEnd === match.end) { - const next = match.end - element.setSelectionRange(next, next) - syncMentionFromCaret(element) - return true - } - element.setSelectionRange(match.start, match.end) - syncMentionFromCaret(element) - return true - } - return false - } - - function renderAttachmentChip(part: PromptAttachmentPart, _placeholder: string) { - const display = part.display ?? createAttachmentDisplay(part.path, part.selection) - return @{display} - } - - function renderTextSegment(value: string) { - if (!value) return undefined - return {value} - } - - function handlePromptKeyDown(event: KeyboardEvent & { currentTarget: HTMLTextAreaElement }) { - if (event.isComposing) return - const target = event.currentTarget - const key = event.key - - const handled = handleMentionKeyDown({ - event, - mentionItems, - insertMention, - }) - if (handled) return - - if (!state.mentionOpen) { - if (key === "ArrowLeft") { - if (handleAttachmentNavigation(event, "left")) return - } - if (key === "ArrowRight") { - if (handleAttachmentNavigation(event, "right")) return - } - } - - if (key === "ArrowLeft" || key === "ArrowRight" || key === "Home" || key === "End") { - queueMicrotask(() => { - syncMentionFromCaret(target) - }) - } - - if (key === "Enter" && !event.shiftKey) { - event.preventDefault() - target.form?.requestSubmit() - } - } - - const handleSubmit = async (event: SubmitEvent) => { - event.preventDefault() - const parts = baseParts() - const text = parts - .map((part) => { - if (part.kind === "text") return part.value - return `@${part.path}` - }) - .join("") - - const currentPrompt: PromptSubmitValue = { - text, - parts, - } - setState("promptInput", "") - resetScrollPosition() - if (inputRef) { - inputRef.blur() - } - - await props.onSubmit(currentPrompt) - } - - onCleanup(() => { - props.onInputRefChange?.(undefined) - }) - - return ( -
-
{ - const evt = event as unknown as globalThis.DragEvent - if (evt.dataTransfer?.types.includes("text/plain")) { - evt.preventDefault() - setState("isDragOver", true) - } - }} - onDragLeave={(event) => { - if (event.currentTarget === event.target) { - setState("isDragOver", false) - } - }} - onDragOver={(event) => { - const evt = event as unknown as globalThis.DragEvent - if (evt.dataTransfer?.types.includes("text/plain")) { - evt.preventDefault() - evt.dataTransfer.dropEffect = "copy" - } - }} - onDrop={(event) => { - const evt = event as unknown as globalThis.DragEvent - evt.preventDefault() - setState("isDragOver", false) - - const data = evt.dataTransfer?.getData("text/plain") - if (data && data.startsWith("file:")) { - const filePath = data.slice(5) - const fileNode = local.file.node(filePath) - if (fileNode) { - local.context.add({ - type: "file", - path: filePath, - }) - } - } - }} - > - 0 || local.context.active()}> -
- - local.context.removeActive()} /> - - - {(file) => local.context.remove(file.key)} />} - -
-
-
- -
{ - overlayContainerRef = element ?? undefined - }} - class="pointer-events-none absolute inset-0 overflow-hidden" - > - -
-
{ - mentionMeasureRef = element ?? undefined - }} - class="pointer-events-none invisible absolute inset-0 whitespace-pre-wrap text-base font-light leading-relaxed px-0.5" - aria-hidden="true" - >
- setState("mentionIndex", index)} - onSelect={insertMention} - /> -
-
-
-