feat(UI): Adding linter and prettier for UI (#3156)

This commit is contained in:
Francisco Arceo 2025-08-14 15:58:43 -06:00 committed by GitHub
parent 61582f327c
commit e69acbafbf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
73 changed files with 1452 additions and 1226 deletions

View file

@ -1,41 +1,41 @@
"use client"
"use client";
import React, { useEffect, useRef, useState } from "react"
import { AnimatePresence, motion } from "framer-motion"
import { ArrowUp, Info, Loader2, Mic, Paperclip, Square } from "lucide-react"
import { omit } from "remeda"
import React, { useEffect, useRef, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { ArrowUp, Info, Loader2, Mic, Paperclip, Square } from "lucide-react";
import { omit } from "remeda";
import { cn } from "@/lib/utils"
import { useAudioRecording } from "@/hooks/use-audio-recording"
import { useAutosizeTextArea } from "@/hooks/use-autosize-textarea"
import { AudioVisualizer } from "@/components/ui/audio-visualizer"
import { Button } from "@/components/ui/button"
import { FilePreview } from "@/components/ui/file-preview"
import { InterruptPrompt } from "@/components/chat-playground/interrupt-prompt"
import { cn } from "@/lib/utils";
import { useAudioRecording } from "@/hooks/use-audio-recording";
import { useAutosizeTextArea } from "@/hooks/use-autosize-textarea";
import { AudioVisualizer } from "@/components/ui/audio-visualizer";
import { Button } from "@/components/ui/button";
import { FilePreview } from "@/components/ui/file-preview";
import { InterruptPrompt } from "@/components/chat-playground/interrupt-prompt";
interface MessageInputBaseProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
value: string
submitOnEnter?: boolean
stop?: () => void
isGenerating: boolean
enableInterrupt?: boolean
transcribeAudio?: (blob: Blob) => Promise<string>
value: string;
submitOnEnter?: boolean;
stop?: () => void;
isGenerating: boolean;
enableInterrupt?: boolean;
transcribeAudio?: (blob: Blob) => Promise<string>;
}
interface MessageInputWithoutAttachmentProps extends MessageInputBaseProps {
allowAttachments?: false
allowAttachments?: false;
}
interface MessageInputWithAttachmentsProps extends MessageInputBaseProps {
allowAttachments: true
files: File[] | null
setFiles: React.Dispatch<React.SetStateAction<File[] | null>>
allowAttachments: true;
files: File[] | null;
setFiles: React.Dispatch<React.SetStateAction<File[] | null>>;
}
type MessageInputProps =
| MessageInputWithoutAttachmentProps
| MessageInputWithAttachmentsProps
| MessageInputWithAttachmentsProps;
export function MessageInput({
placeholder = "Ask AI...",
@ -48,8 +48,8 @@ export function MessageInput({
transcribeAudio,
...props
}: MessageInputProps) {
const [isDragging, setIsDragging] = useState(false)
const [showInterruptPrompt, setShowInterruptPrompt] = useState(false)
const [isDragging, setIsDragging] = useState(false);
const [showInterruptPrompt, setShowInterruptPrompt] = useState(false);
const {
isListening,
@ -61,123 +61,124 @@ export function MessageInput({
stopRecording,
} = useAudioRecording({
transcribeAudio,
onTranscriptionComplete: (text) => {
props.onChange?.({ target: { value: text } } as any)
onTranscriptionComplete: text => {
props.onChange?.({
target: { value: text },
} as React.ChangeEvent<HTMLTextAreaElement>);
},
})
});
useEffect(() => {
if (!isGenerating) {
setShowInterruptPrompt(false)
setShowInterruptPrompt(false);
}
}, [isGenerating])
}, [isGenerating]);
const addFiles = (files: File[] | null) => {
if (props.allowAttachments) {
props.setFiles((currentFiles) => {
props.setFiles(currentFiles => {
if (currentFiles === null) {
return files
return files;
}
if (files === null) {
return currentFiles
return currentFiles;
}
return [...currentFiles, ...files]
})
return [...currentFiles, ...files];
});
}
}
};
const onDragOver = (event: React.DragEvent) => {
if (props.allowAttachments !== true) return
event.preventDefault()
setIsDragging(true)
}
if (props.allowAttachments !== true) return;
event.preventDefault();
setIsDragging(true);
};
const onDragLeave = (event: React.DragEvent) => {
if (props.allowAttachments !== true) return
event.preventDefault()
setIsDragging(false)
}
if (props.allowAttachments !== true) return;
event.preventDefault();
setIsDragging(false);
};
const onDrop = (event: React.DragEvent) => {
setIsDragging(false)
if (props.allowAttachments !== true) return
event.preventDefault()
const dataTransfer = event.dataTransfer
setIsDragging(false);
if (props.allowAttachments !== true) return;
event.preventDefault();
const dataTransfer = event.dataTransfer;
if (dataTransfer.files.length) {
addFiles(Array.from(dataTransfer.files))
addFiles(Array.from(dataTransfer.files));
}
}
};
const onPaste = (event: React.ClipboardEvent) => {
const items = event.clipboardData?.items
if (!items) return
const items = event.clipboardData?.items;
if (!items) return;
const text = event.clipboardData.getData("text")
const text = event.clipboardData.getData("text");
if (text && text.length > 500 && props.allowAttachments) {
event.preventDefault()
const blob = new Blob([text], { type: "text/plain" })
event.preventDefault();
const blob = new Blob([text], { type: "text/plain" });
const file = new File([blob], "Pasted text", {
type: "text/plain",
lastModified: Date.now(),
})
addFiles([file])
return
});
addFiles([file]);
return;
}
const files = Array.from(items)
.map((item) => item.getAsFile())
.filter((file) => file !== null)
.map(item => item.getAsFile())
.filter(file => file !== null);
if (props.allowAttachments && files.length > 0) {
addFiles(files)
addFiles(files);
}
}
};
const onKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (submitOnEnter && event.key === "Enter" && !event.shiftKey) {
event.preventDefault()
event.preventDefault();
if (isGenerating && stop && enableInterrupt) {
if (showInterruptPrompt) {
stop()
setShowInterruptPrompt(false)
event.currentTarget.form?.requestSubmit()
stop();
setShowInterruptPrompt(false);
event.currentTarget.form?.requestSubmit();
} else if (
props.value ||
(props.allowAttachments && props.files?.length)
) {
setShowInterruptPrompt(true)
return
setShowInterruptPrompt(true);
return;
}
}
event.currentTarget.form?.requestSubmit()
event.currentTarget.form?.requestSubmit();
}
onKeyDownProp?.(event)
}
onKeyDownProp?.(event);
};
const textAreaRef = useRef<HTMLTextAreaElement>(null)
const [textAreaHeight, setTextAreaHeight] = useState<number>(0)
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const [textAreaHeight, setTextAreaHeight] = useState<number>(0);
useEffect(() => {
if (textAreaRef.current) {
setTextAreaHeight(textAreaRef.current.offsetHeight)
setTextAreaHeight(textAreaRef.current.offsetHeight);
}
}, [props.value])
}, [props.value]);
const showFileList =
props.allowAttachments && props.files && props.files.length > 0
props.allowAttachments && props.files && props.files.length > 0;
useAutosizeTextArea({
ref: textAreaRef,
maxHeight: 240,
borderWidth: 1,
dependencies: [props.value, showFileList],
})
});
return (
<div
@ -220,24 +221,24 @@ export function MessageInput({
<div className="absolute inset-x-3 bottom-0 z-20 overflow-x-scroll py-3">
<div className="flex space-x-3">
<AnimatePresence mode="popLayout">
{props.files?.map((file) => {
{props.files?.map(file => {
return (
<FilePreview
key={file.name + String(file.lastModified)}
file={file}
onRemove={() => {
props.setFiles((files) => {
if (!files) return null
props.setFiles(files => {
if (!files) return null;
const filtered = Array.from(files).filter(
(f) => f !== file
)
if (filtered.length === 0) return null
return filtered
})
f => f !== file
);
if (filtered.length === 0) return null;
return filtered;
});
}}
/>
)
);
})}
</AnimatePresence>
</div>
@ -256,8 +257,8 @@ export function MessageInput({
aria-label="Attach a file"
disabled={true}
onClick={async () => {
const files = await showFileUploadDialog()
addFiles(files)
const files = await showFileUploadDialog();
addFiles(files);
}}
>
<Paperclip className="h-4 w-4" />
@ -308,12 +309,12 @@ export function MessageInput({
onStopRecording={stopRecording}
/>
</div>
)
);
}
MessageInput.displayName = "MessageInput"
MessageInput.displayName = "MessageInput";
interface FileUploadOverlayProps {
isDragging: boolean
isDragging: boolean;
}
function FileUploadOverlay({ isDragging }: FileUploadOverlayProps) {
@ -333,29 +334,29 @@ function FileUploadOverlay({ isDragging }: FileUploadOverlayProps) {
</motion.div>
)}
</AnimatePresence>
)
);
}
function showFileUploadDialog() {
const input = document.createElement("input")
const input = document.createElement("input");
input.type = "file"
input.multiple = true
input.accept = "*/*"
input.click()
input.type = "file";
input.multiple = true;
input.accept = "*/*";
input.click();
return new Promise<File[] | null>((resolve) => {
input.onchange = (e) => {
const files = (e.currentTarget as HTMLInputElement).files
return new Promise<File[] | null>(resolve => {
input.onchange = e => {
const files = (e.currentTarget as HTMLInputElement).files;
if (files) {
resolve(Array.from(files))
return
resolve(Array.from(files));
return;
}
resolve(null)
}
})
resolve(null);
};
});
}
function TranscribingOverlay() {
@ -385,12 +386,12 @@ function TranscribingOverlay() {
Transcribing audio...
</p>
</motion.div>
)
);
}
interface RecordingPromptProps {
isVisible: boolean
onStopRecording: () => void
isVisible: boolean;
onStopRecording: () => void;
}
function RecordingPrompt({ isVisible, onStopRecording }: RecordingPromptProps) {
@ -418,15 +419,15 @@ function RecordingPrompt({ isVisible, onStopRecording }: RecordingPromptProps) {
</motion.div>
)}
</AnimatePresence>
)
);
}
interface RecordingControlsProps {
isRecording: boolean
isTranscribing: boolean
audioStream: MediaStream | null
textAreaHeight: number
onStopRecording: () => void
isRecording: boolean;
isTranscribing: boolean;
audioStream: MediaStream | null;
textAreaHeight: number;
onStopRecording: () => void;
}
function RecordingControls({
@ -448,7 +449,7 @@ function RecordingControls({
onClick={onStopRecording}
/>
</div>
)
);
}
if (isTranscribing) {
@ -459,8 +460,8 @@ function RecordingControls({
>
<TranscribingOverlay />
</div>
)
);
}
return null
return null;
}