goji-search 1.0.1 → 1.1.2

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.
@@ -1,15 +1,31 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useRef, useEffect, useState, useId } from "react";
2
+ import { useRef, useEffect, useState } from "react";
3
3
  import ReactMarkdown from "react-markdown";
4
4
  import remarkGfm from "remark-gfm";
5
- import { ExternalLink, User, X, Maximize2, Minimize2 } from "lucide-react";
6
- export function MessageList({ messages, isStreaming, aiAvatarSrc = '', onClose, onExpand, size = 'l', }) {
5
+ import { ExternalLink } from "lucide-react";
6
+ export function MessageList({ messages, isStreaming, aiAvatarSrc = '', size = 'l', }) {
7
7
  const messagesEndRef = useRef(null);
8
- const [hoverTopExpand, setHoverTopExpand] = useState(false);
9
- const [hoverTopClose, setHoverTopClose] = useState(false);
10
- const idBase = useId();
11
- const xTooltipId = `${idBase}-x-tip`;
12
- const expandTooltipId = `${idBase}-expand-tip`;
8
+ const [expandedSourcesByMessageIndex, setExpandedSourcesByMessageIndex] = useState({});
9
+ const toggleSources = (messageIndex) => {
10
+ setExpandedSourcesByMessageIndex((prev) => ({
11
+ ...prev,
12
+ [messageIndex]: !prev[messageIndex],
13
+ }));
14
+ };
15
+ const convertCitationLinksToUrls = (markdown, sources) => {
16
+ if (!markdown || !sources || sources.length === 0)
17
+ return markdown;
18
+ // Replace [text](N) where N is a positive integer referencing sources[N-1]
19
+ return markdown.replace(/\[([^\]]+)\]\((\d+)\)/g, (match, text, numStr) => {
20
+ const num = parseInt(numStr, 10);
21
+ if (Number.isNaN(num) || num < 1 || num > sources.length)
22
+ return match;
23
+ const url = sources[num - 1]?.url;
24
+ if (!url)
25
+ return match;
26
+ return `[${text}](${url})`;
27
+ });
28
+ };
13
29
  useEffect(() => {
14
30
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
15
31
  }, [messages]);
@@ -19,101 +35,135 @@ export function MessageList({ messages, isStreaming, aiAvatarSrc = '', onClose,
19
35
  position: 'relative',
20
36
  flex: 1,
21
37
  overflowY: "auto",
22
- marginTop: "0",
38
+ marginTop: "1.3rem",
23
39
  display: "flex",
24
40
  flexDirection: "column",
25
- gap: "0.75rem",
41
+ gap: "0.5rem",
26
42
  padding: "0.5rem",
27
43
  borderRadius: "0.75rem",
28
44
  backgroundColor: "rgba(255, 255, 255, 0.08)",
29
45
  border: "1px solid rgba(255, 255, 255, 0.15)",
30
- }, className: "custom-scrollbar", children: [_jsxs("div", { style: { display: 'flex', gap: '0.5rem', zIndex: 20, justifyContent: 'flex-end', paddingTop: '0' }, children: [onExpand && (_jsxs("div", { style: { position: 'relative' }, children: [_jsx("button", { type: "button", onClick: onExpand, onMouseEnter: () => setHoverTopExpand(true), onMouseLeave: () => setHoverTopExpand(false), "aria-label": size === 'xl' ? 'Restore' : 'Expand', "aria-describedby": hoverTopExpand ? expandTooltipId : undefined, style: {
31
- borderRadius: '1.3rem',
32
- border: '1px solid rgba(255, 255, 255, 0.3)',
33
- backgroundColor: 'rgba(255, 255, 255, 0.12)',
34
- padding: '0.375rem',
35
- display: 'flex',
36
- alignItems: 'center',
37
- justifyContent: 'center',
38
- color: 'rgba(0, 0, 0, 0.75)',
39
- transition: 'all 0.15s',
40
- cursor: 'pointer',
41
- outline: 'none',
42
- boxShadow: 'inset 0 1px 0 rgba(255, 255, 255, 0.2)',
43
- }, children: size === 'xl' ? _jsx(Minimize2, { style: { width: '0.9rem', height: '0.9rem' } }) : _jsx(Maximize2, { style: { width: '0.9rem', height: '0.9rem' } }) }), _jsx("span", { id: expandTooltipId, role: "tooltip", style: {
44
- position: 'absolute',
45
- top: 'calc(100% + 0.4rem)',
46
- left: '50%',
47
- transform: 'translateX(-50%)',
48
- whiteSpace: 'nowrap',
49
- background: 'rgba(0,0,0,0.9)',
50
- color: '#fff',
51
- padding: '4px 6px',
52
- borderRadius: '1.3rem',
53
- fontSize: '0.55rem',
54
- boxShadow: '0 6px 18px rgba(0,0,0,0.35)',
55
- opacity: hoverTopExpand ? 1 : 0,
56
- pointerEvents: 'none',
57
- transition: 'opacity 120ms ease-in-out',
58
- zIndex: 30,
59
- }, children: size === 'xl' ? 'Restore' : 'Expand' })] })), onClose && (_jsxs("div", { style: { position: 'relative' }, children: [_jsx("button", { type: "button", onClick: onClose, onMouseEnter: () => setHoverTopClose(true), onMouseLeave: () => setHoverTopClose(false), "aria-label": "Close", "aria-describedby": hoverTopClose ? xTooltipId : undefined, style: {
60
- borderRadius: '1.3rem',
61
- border: '1px solid rgba(255, 255, 255, 0.3)',
62
- backgroundColor: 'rgba(255, 255, 255, 0.12)',
63
- padding: '0.375rem',
64
- display: 'flex',
65
- alignItems: 'center',
66
- justifyContent: 'center',
67
- color: 'rgba(0, 0, 0, 0.75)',
68
- transition: 'all 0.15s',
69
- cursor: 'pointer',
70
- outline: 'none',
71
- boxShadow: 'inset 0 1px 0 rgba(255, 255, 255, 0.2)',
72
- }, children: _jsx(X, { style: { width: '0.9rem', height: '0.9rem' } }) }), _jsx("span", { id: xTooltipId, role: "tooltip", style: {
73
- position: 'absolute',
74
- top: 'calc(100% + 0.4rem)',
75
- left: '50%',
76
- transform: 'translateX(-50%)',
77
- whiteSpace: 'nowrap',
78
- background: 'rgba(0,0,0,0.9)',
79
- color: '#fff',
80
- padding: '4px 6px',
81
- borderRadius: '1.3rem',
82
- fontSize: '0.55rem',
83
- boxShadow: '0 6px 18px rgba(0,0,0,0.35)',
84
- opacity: hoverTopClose ? 1 : 0,
85
- pointerEvents: 'none',
86
- transition: 'opacity 120ms ease-in-out',
87
- zIndex: 30,
88
- }, children: "Close" })] }))] }), messages.map((message, index) => (_jsxs("div", { style: {
89
- display: "flex",
90
- flexDirection: "column",
91
- alignItems: message.role === "user" ? "flex-end" : "flex-start",
92
- animation: "fadeIn 0.3s ease-in",
93
- gap: "0.375rem",
94
- }, children: [_jsx("div", { style: {
46
+ }, className: "custom-scrollbar", children: [messages.map((message, index) => {
47
+ // Skip rendering empty assistant messages during streaming (thinking dots will show instead)
48
+ const isEmptyAssistantWhileStreaming = message.role === "assistant" && !message.content && isStreaming;
49
+ if (isEmptyAssistantWhileStreaming)
50
+ return null;
51
+ return (_jsxs("div", { style: {
52
+ display: "flex",
53
+ justifyContent: message.role === "user" ? "flex-end" : "flex-start",
54
+ animation: "fadeIn 0.3s ease-in",
55
+ gap: "0.4rem",
56
+ }, children: [_jsx("div", { style: {
57
+ display: "flex",
58
+ gap: "0.4rem",
59
+ alignItems: message.role === "user" ? "flex-end" : "flex-start"
60
+ }, children: _jsx("div", { style: {
61
+ width: message.role === "user" ? "0" : "1.4rem",
62
+ height: message.role === "user" ? "0" : "auto",
63
+ borderRadius: "none",
64
+ backgroundColor: "transparent",
65
+ display: "flex",
66
+ alignItems: "center",
67
+ justifyContent: "center",
68
+ fontSize: "0.75rem",
69
+ border: "none",
70
+ boxShadow: "none",
71
+ }, children: message.role === "user" ? (
72
+ // <User size={16} color="rgba(0, 0, 0, 1)" />
73
+ _jsx("div", {})) : (aiAvatarSrc ? (_jsx("img", { src: aiAvatarSrc, alt: "AI Avatar", style: {
74
+ width: "100%",
75
+ height: "100%",
76
+ objectFit: "cover"
77
+ } })) : (_jsx("div", { style: {
78
+ width: "100%",
79
+ height: "100%",
80
+ display: "flex",
81
+ alignItems: "center",
82
+ justifyContent: "center",
83
+ fontSize: "0.75rem",
84
+ color: "rgba(217, 219, 234, 1)"
85
+ }, children: "AI" }))) }) }), _jsxs("div", { style: {
86
+ maxWidth: "85%",
87
+ padding: message.role === "user" ? "0.5rem 0.75rem" : "0.5rem 0",
88
+ borderRadius: message.role === "user" ? "1.2rem" : "0",
89
+ fontSize: "0.875rem",
90
+ marginLeft: message.role === "user" ? "0" : "1rem",
91
+ marginRight: message.role === "user" ? "1rem" : "0",
92
+ lineHeight: "1.4",
93
+ backgroundColor: message.role === "user" ? "rgb(114, 126, 237)" : "transparent",
94
+ color: message.role === "user" ? "white" : "rgba(0, 0, 0, 0.85)",
95
+ border: message.role === "user" ? "1px solid rgba(255, 255, 255, 0.35)" : "none",
96
+ boxShadow: message.role === "user" ? "0 2px 8px rgba(114, 126, 237, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.25)" : "none",
97
+ }, children: [message.role === "user" ? (_jsx("p", { style: { margin: 0, whiteSpace: "pre-wrap" }, children: message.content })) : (_jsx("div", { className: "markdown-content", style: {
98
+ fontSize: "0.875rem",
99
+ lineHeight: "1.4",
100
+ color: "rgba(0, 0, 0, 0.85)",
101
+ }, children: _jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], components: {
102
+ a: ({ href, children, ...props }) => {
103
+ const hasSources = Array.isArray(message.sources) && message.sources.length > 0;
104
+ if (!hasSources) {
105
+ const anyProps = props;
106
+ return (_jsx("span", { className: anyProps?.className, style: anyProps?.style, children: children }));
107
+ }
108
+ return (_jsx("a", { href: href || '', target: "_blank", rel: "noopener noreferrer", ...props, children: children }));
109
+ },
110
+ }, children: convertCitationLinksToUrls(message.content, message.sources) }) })), message.role === "assistant" && message.sources && message.sources.length > 0 && (_jsxs("div", { style: {
111
+ marginTop: "0.25rem",
112
+ paddingTop: "0.25rem",
113
+ borderTop: "1px solid rgba(0, 0, 0, 0.1)",
114
+ }, children: [_jsx("div", { style: {
115
+ display: "flex",
116
+ alignItems: "center",
117
+ justifyContent: "flex-end",
118
+ gap: "0.5rem",
119
+ marginBottom: "0.25rem",
120
+ }, children: _jsx("button", { type: "button", onClick: () => toggleSources(index), style: {
121
+ borderRadius: "0.5rem",
122
+ border: "1px solid rgba(255, 255, 255, 0.3)",
123
+ backgroundColor: "rgba(255, 255, 255, 0.2)",
124
+ padding: "0.2rem 0.4rem",
125
+ fontSize: "0.65rem",
126
+ color: "rgba(0, 0, 0, 0.75)",
127
+ cursor: "pointer",
128
+ outline: "none",
129
+ textTransform: "none",
130
+ }, children: expandedSourcesByMessageIndex[index] ? "Hide sources" : "Show sources" }) }), expandedSourcesByMessageIndex[index] && (_jsx("div", { style: { display: "flex", flexDirection: "column", gap: "0.5rem" }, children: message.sources.map((source) => (_jsxs("a", { href: source.url, target: "_blank", rel: "noopener noreferrer", style: {
131
+ display: "flex",
132
+ alignItems: "center",
133
+ gap: "0.5rem",
134
+ fontSize: "0.75rem",
135
+ color: "rgba(0, 0, 0, 0.7)",
136
+ textDecoration: "none",
137
+ transition: "color 0.2s",
138
+ }, onMouseEnter: (e) => {
139
+ e.currentTarget.style.color = "rgba(0, 0, 0, 0.9)";
140
+ }, onMouseLeave: (e) => {
141
+ e.currentTarget.style.color = "rgba(0, 0, 0, 0.7)";
142
+ }, children: [_jsxs("span", { style: {
143
+ backgroundColor: "rgba(0, 0, 0, 0.1)",
144
+ padding: "0.125rem 0.375rem",
145
+ borderRadius: "0.375rem",
146
+ fontSize: "0.65rem",
147
+ fontWeight: 600,
148
+ }, children: ["#", source.index] }), _jsx("span", { style: { flex: 1, wordBreak: "break-all" }, children: source.url }), _jsx(ExternalLink, { style: { width: "0.875rem", height: "0.875rem", opacity: 0.6 } })] }, source.index))) }))] }))] })] }, index));
149
+ }), showThinkingDots && (_jsxs("div", { style: { display: "flex", justifyContent: "flex-start", gap: "0.4rem", alignItems: "flex-start" }, children: [_jsx("div", { style: {
95
150
  display: "flex",
96
- alignItems: "center",
97
- gap: "0.5rem",
98
- flexDirection: message.role === "user" ? "row-reverse" : "row"
151
+ gap: "0.4rem",
152
+ alignItems: "flex-start"
99
153
  }, children: _jsx("div", { style: {
100
- width: "1.75rem",
101
- height: "1.75rem",
102
- borderRadius: message.role === "user" ? "50%" : "0.75rem",
103
- backgroundColor: message.role === "user" ? "rgba(255, 255, 255, 0.15)" : "rgba(114, 126, 237, 0.15)",
154
+ width: "1.4rem",
155
+ height: "auto",
156
+ borderRadius: "none",
157
+ backgroundColor: "transparent",
104
158
  display: "flex",
105
159
  alignItems: "center",
106
160
  justifyContent: "center",
107
161
  fontSize: "0.75rem",
108
- color: "rgb(114, 126, 237)",
109
- border: message.role === "user"
110
- ? "1px solid rgba(114, 126, 237, 0.3)"
111
- : "1px dashed rgba(114, 126, 237, 0.3)",
112
- boxShadow: message.role === "user" ? "0 2px 4px rgba(0,0,0,0.05)" : "none",
113
- }, children: message.role === "user" ? (_jsx(User, { size: 16, color: "rgb(114, 126, 237)" })) : (aiAvatarSrc ? (_jsx("img", { src: aiAvatarSrc, alt: "AI Avatar", style: {
162
+ border: "none",
163
+ boxShadow: "none",
164
+ }, children: aiAvatarSrc ? (_jsx("img", { src: aiAvatarSrc, alt: "AI Avatar", style: {
114
165
  width: "100%",
115
166
  height: "100%",
116
- borderRadius: "inherit",
117
167
  objectFit: "cover"
118
168
  } })) : (_jsx("div", { style: {
119
169
  width: "100%",
@@ -122,75 +172,33 @@ export function MessageList({ messages, isStreaming, aiAvatarSrc = '', onClose,
122
172
  alignItems: "center",
123
173
  justifyContent: "center",
124
174
  fontSize: "0.75rem",
125
- color: "rgb(114, 126, 237)"
126
- }, children: "AI" }))) }) }), _jsxs("div", { style: {
127
- maxWidth: "85%",
128
- padding: message.role === "user" ? "0.275rem 0.75rem" : "0.5rem 0",
129
- borderRadius: message.role === "user" ? "1rem" : "0",
130
- fontSize: "0.875rem",
131
- marginLeft: message.role === "user" ? "0" : "1rem",
132
- marginRight: message.role === "user" ? "1rem" : "0",
133
- lineHeight: "1.6",
134
- backgroundColor: message.role === "user" ? "rgb(114, 126, 237)" : "transparent",
135
- color: message.role === "user" ? "white" : "rgba(0, 0, 0, 0.85)",
136
- border: message.role === "user" ? "1px solid rgba(255, 255, 255, 0.25)" : "none",
137
- }, children: [message.role === "user" ? (_jsx("p", { style: { margin: 0, whiteSpace: "pre-wrap" }, children: message.content })) : (_jsx("div", { className: "markdown-content", style: {
138
- fontSize: "0.875rem",
139
- lineHeight: "1.7",
140
- color: "rgba(0, 0, 0, 0.85)",
141
- }, children: _jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], children: message.content }) })), message.role === "assistant" && message.sources && message.sources.length > 0 && (_jsxs("div", { style: {
142
- marginTop: "1rem",
143
- paddingTop: "0.75rem",
144
- borderTop: "1px solid rgba(0, 0, 0, 0.1)",
145
- }, children: [_jsx("p", { style: {
146
- fontSize: "0.65rem",
147
- fontWeight: 600,
148
- textTransform: "uppercase",
149
- letterSpacing: "0.05em",
150
- color: "rgba(0, 0, 0, 0.5)",
151
- marginBottom: "0.5rem",
152
- }, children: "Sources" }), _jsx("div", { style: { display: "flex", flexDirection: "column", gap: "0.5rem" }, children: message.sources.map((source) => (_jsxs("a", { href: source.url, target: "_blank", rel: "noopener noreferrer", style: {
153
- display: "flex",
154
- alignItems: "center",
155
- gap: "0.5rem",
156
- fontSize: "0.75rem",
157
- color: "rgba(0, 0, 0, 0.7)",
158
- textDecoration: "none",
159
- transition: "color 0.2s",
160
- }, onMouseEnter: (e) => {
161
- e.currentTarget.style.color = "rgba(0, 0, 0, 0.9)";
162
- }, onMouseLeave: (e) => {
163
- e.currentTarget.style.color = "rgba(0, 0, 0, 0.7)";
164
- }, children: [_jsxs("span", { style: {
165
- backgroundColor: "rgba(0, 0, 0, 0.1)",
166
- padding: "0.125rem 0.375rem",
167
- borderRadius: "0.375rem",
168
- fontSize: "0.65rem",
169
- fontWeight: 600,
170
- }, children: ["#", source.index] }), _jsx("span", { style: { flex: 1, wordBreak: "break-all" }, children: source.url }), _jsx(ExternalLink, { style: { width: "0.875rem", height: "0.875rem", opacity: 0.6 } })] }, source.index))) })] }))] })] }, index))), showThinkingDots && (_jsx("div", { style: { display: "flex", justifyContent: "flex-start" }, children: _jsxs("div", { style: {
171
- padding: "0.75rem 0.6rem",
172
- borderRadius: "1rem",
173
- display: "flex",
174
- gap: "0.375rem",
175
- }, children: [_jsx("span", { style: {
176
- width: "0.5rem",
177
- height: "0.5rem",
178
- borderRadius: "50%",
179
- backgroundColor: "rgba(0, 0, 0, 0.5)",
180
- animation: "pulse 1.4s ease-in-out infinite",
181
- } }), _jsx("span", { style: {
182
- width: "0.5rem",
183
- height: "0.5rem",
184
- borderRadius: "50%",
185
- backgroundColor: "rgba(0, 0, 0, 0.5)",
186
- animation: "pulse 1.4s ease-in-out 0.2s infinite",
187
- } }), _jsx("span", { style: {
188
- width: "0.5rem",
189
- height: "0.5rem",
190
- borderRadius: "50%",
191
- backgroundColor: "rgba(0, 0, 0, 0.5)",
192
- animation: "pulse 1.4s ease-in-out 0.4s infinite",
193
- } })] }) })), _jsx("div", { ref: messagesEndRef }), _jsx("style", { children: `
175
+ color: "rgba(217, 219, 234, 1)"
176
+ }, children: "AI" })) }) }), _jsxs("div", { style: {
177
+ marginLeft: "1rem",
178
+ padding: "0.25rem 0.5rem",
179
+ borderRadius: "1rem",
180
+ display: "flex",
181
+ gap: "0.25rem",
182
+ alignItems: "center",
183
+ }, children: [_jsx("span", { style: {
184
+ width: "0.35rem",
185
+ height: "0.35rem",
186
+ borderRadius: "50%",
187
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
188
+ animation: "pulse 1.4s ease-in-out infinite",
189
+ } }), _jsx("span", { style: {
190
+ width: "0.35rem",
191
+ height: "0.35rem",
192
+ borderRadius: "50%",
193
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
194
+ animation: "pulse 1.4s ease-in-out 0.2s infinite",
195
+ } }), _jsx("span", { style: {
196
+ width: "0.35rem",
197
+ height: "0.35rem",
198
+ borderRadius: "50%",
199
+ backgroundColor: "rgba(0, 0, 0, 0.5)",
200
+ animation: "pulse 1.4s ease-in-out 0.4s infinite",
201
+ } })] })] })), _jsx("div", { ref: messagesEndRef }), _jsx("style", { children: `
194
202
  .markdown-content p {
195
203
  margin: 0 0 0.75rem 0;
196
204
  }
@@ -1,15 +1,17 @@
1
1
  import type React from "react";
2
2
  interface SearchInputProps {
3
3
  value: string;
4
- onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
4
+ onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
5
5
  onFocus: () => void;
6
6
  onBlur: () => void;
7
7
  placeholder: string;
8
8
  size: "xs" | "s" | "m" | "l" | "xl";
9
- inputRef: React.RefObject<HTMLInputElement>;
9
+ setSize: (size: "xs" | "s" | "m" | "l" | "xl") => void;
10
+ inputRef: React.RefObject<HTMLTextAreaElement>;
10
11
  inspirationQuestions: string[];
11
12
  onInspirationClick: (question: string) => void;
12
13
  sparkleRef: React.RefObject<HTMLDivElement>;
14
+ suggestedLabel?: string;
13
15
  }
14
- export declare function SearchInput({ value, onChange, onFocus, onBlur, placeholder, size, inputRef, inspirationQuestions, onInspirationClick, sparkleRef, }: SearchInputProps): import("react/jsx-runtime").JSX.Element;
16
+ export declare function SearchInput({ value, onChange, onFocus, onBlur, placeholder, size, setSize, inputRef, inspirationQuestions, onInspirationClick, sparkleRef, suggestedLabel, }: SearchInputProps): import("react/jsx-runtime").JSX.Element;
15
17
  export {};
@@ -2,44 +2,47 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Sparkles } from "lucide-react";
3
3
  import { useEffect, useState } from "react";
4
4
  import { InspirationMenu } from "./inspiration-menu";
5
- export function SearchInput({ value, onChange, onFocus, onBlur, placeholder, size, inputRef, inspirationQuestions, onInspirationClick, sparkleRef, }) {
5
+ import AutoExpandingTextarea from "./auto-expanding-textarea"; // make sure path matches
6
+ export function SearchInput({ value, onChange, onFocus, onBlur, placeholder, size, setSize, inputRef, inspirationQuestions, onInspirationClick, sparkleRef, suggestedLabel = "Suggested", }) {
6
7
  const [showInspirationMenu, setShowInspirationMenu] = useState(false);
7
8
  const [isSparkleHovered, setIsSparkleHovered] = useState(false);
8
9
  const [typewriterText, setTypewriterText] = useState("");
9
10
  const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
10
11
  const [isTyping, setIsTyping] = useState(true);
11
12
  const [charIndex, setCharIndex] = useState(0);
13
+ const [isSingleLine, setIsSingleLine] = useState(true);
12
14
  useEffect(() => {
13
- if (size !== "s" || value || inspirationQuestions.length === 0) {
14
- return;
15
+ if (inputRef.current) {
16
+ const textarea = inputRef.current;
17
+ const lineHeight = 24; // match your `line-height` in CSS (1.5rem ≈ 24px)
18
+ setIsSingleLine(textarea.scrollHeight <= lineHeight * 1.5);
15
19
  }
20
+ }, [value]);
21
+ // Typewriter animation for placeholder
22
+ useEffect(() => {
23
+ if (size !== "s" || value || inspirationQuestions.length === 0)
24
+ return;
16
25
  const currentQuestion = inspirationQuestions[currentQuestionIndex];
17
- const typingSpeed = 40; // ms per character when typing
18
- const deletingSpeed = 20; // ms per character when deleting
19
- const pauseAfterTyping = 2000; // pause when fully typed
20
- const pauseAfterDeleting = 500; // pause before typing next question
26
+ const typingSpeed = 40;
27
+ const deletingSpeed = 20;
28
+ const pauseAfterTyping = 2000;
29
+ const pauseAfterDeleting = 500;
21
30
  const timer = setTimeout(() => {
22
31
  if (isTyping) {
23
- // Typing phase
24
32
  if (charIndex < currentQuestion.length) {
25
33
  setTypewriterText(currentQuestion.slice(0, charIndex + 1));
26
34
  setCharIndex(charIndex + 1);
27
35
  }
28
36
  else {
29
- // Finished typing, pause then start deleting
30
- setTimeout(() => {
31
- setIsTyping(false);
32
- }, pauseAfterTyping);
37
+ setTimeout(() => setIsTyping(false), pauseAfterTyping);
33
38
  }
34
39
  }
35
40
  else {
36
- // Deleting phase
37
41
  if (charIndex > 0) {
38
42
  setTypewriterText(currentQuestion.slice(0, charIndex - 1));
39
43
  setCharIndex(charIndex - 1);
40
44
  }
41
45
  else {
42
- // Finished deleting, move to next question
43
46
  setTimeout(() => {
44
47
  setCurrentQuestionIndex((currentQuestionIndex + 1) % inspirationQuestions.length);
45
48
  setIsTyping(true);
@@ -53,38 +56,81 @@ export function SearchInput({ value, onChange, onFocus, onBlur, placeholder, siz
53
56
  setShowInspirationMenu(!showInspirationMenu);
54
57
  };
55
58
  const handleQuestionClick = (question) => {
59
+ if (size !== "l" && size !== "xl") {
60
+ setSize("l");
61
+ }
56
62
  onInspirationClick(question);
57
63
  setShowInspirationMenu(false);
58
64
  };
59
65
  const displayPlaceholder = size === "s" && !value ? typewriterText : placeholder;
60
- return (_jsxs("div", { style: { position: "relative" }, children: [_jsxs("div", { ref: sparkleRef, onClick: handleSparkleClick, onMouseEnter: () => setIsSparkleHovered(true), onMouseLeave: () => setIsSparkleHovered(false), style: {
66
+ return (_jsxs("div", { style: { position: "relative", zIndex: 1 }, children: [size !== "s" && (_jsxs("div", { ref: sparkleRef, onClick: handleSparkleClick, onMouseEnter: () => {
67
+ setIsSparkleHovered(true);
68
+ setShowInspirationMenu(true);
69
+ }, onMouseLeave: () => {
70
+ setIsSparkleHovered(false);
71
+ // Don't immediately close, wait for user to move to menu
72
+ setTimeout(() => {
73
+ if (!document.querySelector('[data-inspiration-menu]:hover')) {
74
+ setShowInspirationMenu(false);
75
+ }
76
+ }, 150);
77
+ }, style: {
61
78
  position: "absolute",
62
79
  left: "0.75rem",
63
- top: "50%",
64
- transform: "translateY(-50%)",
80
+ top: "0.42rem",
65
81
  cursor: "pointer",
66
- zIndex: 10,
67
- }, children: [_jsx(Sparkles, { style: {
68
- width: size === "s" ? "0.875rem" : "1rem",
69
- height: size === "s" ? "0.875rem" : "1rem",
82
+ // Keep the trigger above the input; menu has an even higher z-index
83
+ zIndex: 2,
84
+ }, children: [_jsx(Sparkles, { className: "sparkle-icon-animated", style: {
85
+ width: "1rem",
86
+ height: "1rem",
70
87
  color: "rgba(0, 0, 0, 0.6)",
71
88
  transition: "all 0.3s ease",
72
- } }), showInspirationMenu && (_jsx(InspirationMenu, { questions: inspirationQuestions, onQuestionClick: handleQuestionClick, onClose: () => setShowInspirationMenu(false) }))] }), _jsx("input", { ref: inputRef, type: "text", value: value, onChange: onChange, onFocus: onFocus, className: "sparkle-input", onBlur: onBlur, placeholder: displayPlaceholder, style: {
73
- width: "100%",
74
- borderRadius: "1.3rem",
75
- border: "1px solid rgba(255, 255, 255, 0.25)",
76
- backgroundColor: "rgba(255, 255, 255, 0.3)",
77
- padding: size === "s" ? "0.375rem 0.5rem 0.375rem 2rem" : "0.5rem 0.75rem 0.5rem 2.25rem",
78
- fontSize: size === "s" ? "0.8125rem" : "0.875rem",
79
- color: "rgba(0, 0, 0, 0.85)",
80
- backdropFilter: "blur(20px) saturate(180%)",
81
- WebkitBackdropFilter: "blur(20px) saturate(180%)",
82
- transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
89
+ } }), showInspirationMenu && (_jsx(InspirationMenu, { questions: inspirationQuestions, onQuestionClick: handleQuestionClick, onClose: () => setShowInspirationMenu(false), suggestedLabel: suggestedLabel }))] })), _jsx(AutoExpandingTextarea, { ref: inputRef, value: value, onChange: onChange, onFocus: onFocus, onMouseEnter: () => {
90
+ if (size === "m") {
91
+ setShowInspirationMenu(true);
92
+ }
93
+ }, onBlur: onBlur, placeholder: displayPlaceholder, className: `sparkle-input ${isSparkleHovered ? "sparkle-hover" : ""}`, style: {
94
+ padding: size === "s" ? "0 0.75rem" : "0 0.75rem 0 2rem",
83
95
  outline: "none",
84
- boxShadow: "inset 0 1px 0 rgba(255, 255, 255, 0.4)",
96
+ transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
97
+ borderRadius: isSingleLine ? "9999px" : "15px",
85
98
  } }), _jsx("style", { children: `
86
- input::placeholder {
87
- color: rgba(0, 0, 0, 0.75);
88
- }
89
- ` })] }));
99
+ .sparkle-input {
100
+ width: 100%;
101
+ border: 1px solid rgba(255, 255, 255, 0.25);
102
+ background-color: transparent;
103
+ border-radius: 9999px; /* full rounded */
104
+ font-size: 0.85rem; /* ~14px */
105
+ color: rgba(0, 0, 0, 0.85);
106
+ backdrop-filter: blur(20px) saturate(180%);
107
+ -webkit-backdrop-filter: blur(20px) saturate(180%);
108
+ resize: none;
109
+ outline: none;
110
+ overflow: hidden; /* hide scrollbar toggles */
111
+ line-height: 1.5rem;
112
+ min-height: 1.5rem;
113
+ max-height: 160px;
114
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
115
+ }
116
+
117
+ .sparkle-input::placeholder {
118
+ color: rgba(0, 0, 0, 0.75);
119
+ }
120
+
121
+ .sparkle-icon-animated {
122
+ animation: sparkleGlow 2s ease-in-out infinite;
123
+ }
124
+
125
+ @keyframes sparkleGlow {
126
+ 0%, 100% {
127
+ opacity: 0.6;
128
+ transform: scale(1);
129
+ }
130
+ 50% {
131
+ opacity: 1;
132
+ transform: scale(1.1);
133
+ }
134
+ }
135
+ ` })] }));
90
136
  }