@sero-ai/ui 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,117 @@
1
+ "use client";
2
+
3
+ import type {
4
+ ChangeEvent,
5
+ ChangeEventHandler,
6
+ ClipboardEventHandler,
7
+ ComponentProps,
8
+ KeyboardEventHandler,
9
+ } from "react";
10
+ import { InputGroupTextarea } from "../ui/input-group";
11
+ import { cn } from "../../lib/utils";
12
+ import { useCallback, useState } from "react";
13
+ import {
14
+ useOptionalPromptInputController,
15
+ usePromptInputAttachments,
16
+ } from "./prompt-input-context";
17
+
18
+ export type PromptInputTextareaProps = ComponentProps<typeof InputGroupTextarea>;
19
+
20
+ export const PromptInputTextarea = ({
21
+ onChange,
22
+ onKeyDown,
23
+ className,
24
+ placeholder = "What would you like to know?",
25
+ ...props
26
+ }: PromptInputTextareaProps) => {
27
+ const controller = useOptionalPromptInputController();
28
+ const attachments = usePromptInputAttachments();
29
+ const [isComposing, setIsComposing] = useState(false);
30
+
31
+ const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = useCallback(
32
+ (e) => {
33
+ // Call the external onKeyDown handler first
34
+ onKeyDown?.(e);
35
+
36
+ // If the external handler prevented default, don't run internal logic
37
+ if (e.defaultPrevented) return;
38
+
39
+ if (e.key === "Enter") {
40
+ if (isComposing || e.nativeEvent.isComposing) return;
41
+ if (e.shiftKey) return;
42
+ e.preventDefault();
43
+
44
+ // Check if the submit button is disabled before submitting
45
+ const { form } = e.currentTarget;
46
+ const submitButton = form?.querySelector(
47
+ 'button[type="submit"]'
48
+ ) as HTMLButtonElement | null;
49
+ if (submitButton?.disabled) return;
50
+
51
+ form?.requestSubmit();
52
+ }
53
+
54
+ // Remove last attachment when Backspace is pressed and textarea is empty
55
+ if (
56
+ e.key === "Backspace" &&
57
+ e.currentTarget.value === "" &&
58
+ attachments.files.length > 0
59
+ ) {
60
+ e.preventDefault();
61
+ const lastAttachment = attachments.files.at(-1);
62
+ if (lastAttachment) {
63
+ attachments.remove(lastAttachment.id);
64
+ }
65
+ }
66
+ },
67
+ [onKeyDown, isComposing, attachments]
68
+ );
69
+
70
+ const handlePaste: ClipboardEventHandler<HTMLTextAreaElement> = useCallback(
71
+ (event) => {
72
+ const items = event.clipboardData?.items;
73
+ if (!items) return;
74
+
75
+ const files: File[] = [];
76
+ for (const item of items) {
77
+ if (item.kind === "file") {
78
+ const file = item.getAsFile();
79
+ if (file) files.push(file);
80
+ }
81
+ }
82
+
83
+ if (files.length > 0) {
84
+ event.preventDefault();
85
+ attachments.add(files);
86
+ }
87
+ },
88
+ [attachments]
89
+ );
90
+
91
+ const handleCompositionEnd = useCallback(() => setIsComposing(false), []);
92
+ const handleCompositionStart = useCallback(() => setIsComposing(true), []);
93
+
94
+ const controlledProps = controller
95
+ ? {
96
+ onChange: (e: ChangeEvent<HTMLTextAreaElement>) => {
97
+ controller.textInput.setInput(e.currentTarget.value);
98
+ onChange?.(e as Parameters<ChangeEventHandler<HTMLTextAreaElement>>[0]);
99
+ },
100
+ value: controller.textInput.value,
101
+ }
102
+ : { onChange };
103
+
104
+ return (
105
+ <InputGroupTextarea
106
+ className={cn("field-sizing-content max-h-48 min-h-16", className)}
107
+ name="message"
108
+ onCompositionEnd={handleCompositionEnd}
109
+ onCompositionStart={handleCompositionStart}
110
+ onKeyDown={handleKeyDown}
111
+ onPaste={handlePaste}
112
+ placeholder={placeholder}
113
+ {...props}
114
+ {...controlledProps}
115
+ />
116
+ );
117
+ };