import { For, Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js" import { Button, FileIcon, Icon, IconButton, Tooltip } from "@/ui" import { Select } from "@/components/select" import { useLocal } from "@/context" import type { FileContext, LocalFile } from "@/context/local" import { getFilename } from "@/utils" import { createSpeechRecognition } from "@/utils/speech" interface PromptFormProps { class?: string classList?: Record onSubmit: (prompt: string) => Promise | void onOpenModelSelect: () => void onInputRefChange?: (element: HTMLTextAreaElement | undefined) => void } export default function PromptForm(props: PromptFormProps) { const local = useLocal() const [prompt, setPrompt] = createSignal("") const [isDragOver, setIsDragOver] = createSignal(false) const placeholderText = "Start typing or speaking..." const { isSupported, isRecording, interim: interimTranscript, start: startSpeech, stop: stopSpeech, } = createSpeechRecognition({ onFinal: (text) => setPrompt((prev) => (prev && !prev.endsWith(" ") ? prev + " " : prev) + text), }) let inputRef: HTMLTextAreaElement | undefined = undefined let overlayContainerRef: HTMLDivElement | undefined = undefined let shouldAutoScroll = true const promptContent = createMemo(() => { const base = prompt() || "" const interim = isRecording() ? interimTranscript() : "" if (!base && !interim) { return {placeholderText} } const needsSpace = base && interim && !base.endsWith(" ") && !interim.startsWith(" ") return ( <> {base} {interim && ( {needsSpace ? " " : ""} {interim} )} ) }) createEffect(() => { prompt() interimTranscript() queueMicrotask(() => { if (!inputRef) return if (!overlayContainerRef) return if (!shouldAutoScroll) { overlayContainerRef.scrollTop = inputRef.scrollTop return } scrollPromptToEnd() }) }) const handlePromptKeyDown = (event: KeyboardEvent & { currentTarget: HTMLTextAreaElement }) => { if (event.isComposing) return if (event.key === "Enter" && !event.shiftKey) { event.preventDefault() inputRef?.form?.requestSubmit() } } const handlePromptScroll = (event: Event & { currentTarget: HTMLTextAreaElement }) => { const target = event.currentTarget shouldAutoScroll = target.scrollTop + target.clientHeight >= target.scrollHeight - 4 if (overlayContainerRef) overlayContainerRef.scrollTop = target.scrollTop } const scrollPromptToEnd = () => { if (!inputRef) return const maxInputScroll = inputRef.scrollHeight - inputRef.clientHeight const next = maxInputScroll > 0 ? maxInputScroll : 0 inputRef.scrollTop = next if (overlayContainerRef) overlayContainerRef.scrollTop = next shouldAutoScroll = true } const handleSubmit = async (event: SubmitEvent) => { event.preventDefault() const currentPrompt = prompt() setPrompt("") shouldAutoScroll = true if (overlayContainerRef) overlayContainerRef.scrollTop = 0 if (inputRef) { inputRef.scrollTop = 0 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() setIsDragOver(true) } }} onDragLeave={(event) => { if (event.currentTarget === event.target) { setIsDragOver(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() setIsDragOver(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" >
{promptContent()}