Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function Message({ message }: { message: webllm.ChatCompletionMessageParam }) {
);
},
}}
className="text-gray-700 pl-8 mt-2 leading-[1.75] prose"
className="text-gray-700 pl-8 mt-2 leading-[1.75] prose break-words"
>
{typeof message.content === "string"
? message.content
Expand Down
38 changes: 28 additions & 10 deletions src/components/UserInput.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useRef } from "react";
import { FaArrowUp } from "react-icons/fa6";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Textarea } from "./ui/textarea";
import useChatStore from "../hooks/useChatStore";
import { MODEL_DESCRIPTIONS } from "../models";

Expand All @@ -16,20 +17,37 @@ function UserInput({
const selectedModel = useChatStore((state) => state.selectedModel);
const isGenerating = useChatStore((state) => state.isGenerating);

const textareaRef = useRef<HTMLTextAreaElement>(null);

const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};

const handleSend = async () => {
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
}

await onSend();
};

return (
<div className="p-4 py-2">
<div className="flex items-center p-2 border rounded-xl shadow-sm">
<Input
className="flex-1 border-none shadow-none focus:ring-0
ring-0 focus:border-0 focus-visible:ring-0 text-base"
<div className="relative flex items-end p-2 border rounded-xl shadow-sm">
<Textarea
ref={textareaRef}
autosize
rows={1}
className="flex-1 max-h-[320px] pb-[6px] border-none shadow-none focus:ring-0
ring-0 focus:border-0 focus-visible:ring-0 text-base
resize-none"
placeholder={`Message ${MODEL_DESCRIPTIONS[selectedModel].displayName}`}
onChange={(e) => setUserInput(e.target.value)}
value={userInput}
onKeyDown={(e) => {
if (e.key === "Enter") {
onSend();
}
}}
onKeyDown={handleKeyDown}
/>
{!isGenerating && (
<Button className="p-2" variant="ghost" onClick={onSend}>
Expand Down
53 changes: 53 additions & 0 deletions src/components/ui/textarea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as React from "react";
import { cn } from "@/lib/utils";
import { useEffect, useRef } from "react";

export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
autosize?: boolean;
}

const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, autosize, ...props }, ref) => {
const innerRef = useRef<HTMLTextAreaElement>(null);
const textareaRef = ref || innerRef;

useEffect(() => {
const resizeTextarea = () => {
if (textareaRef && 'current' in textareaRef && textareaRef.current) {
const textarea = textareaRef.current;
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + 'px';
}
};

if (autosize) {
resizeTextarea();
window.addEventListener('resize', resizeTextarea);
return () => window.removeEventListener('resize', resizeTextarea);
}
}, [autosize]);

return (
<textarea
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={textareaRef}
{...props}
onChange={(event) => {
if (autosize) {
const textarea = event.target as HTMLTextAreaElement;
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + 'px';
}
if (props.onChange) props.onChange(event);
}}
/>
);
}
);

Textarea.displayName = "Textarea";

export { Textarea };