@optilogic/chat 1.0.0-beta.1 → 1.0.0-beta.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.
@@ -0,0 +1,60 @@
1
+ import * as React from "react";
2
+ import { cn } from "@optilogic/core";
3
+
4
+ export interface UserPromptProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ /** The text content of the user's message */
6
+ content: string;
7
+ /** Optional timestamp to display below the message */
8
+ timestamp?: Date;
9
+ }
10
+
11
+ /**
12
+ * UserPrompt component
13
+ *
14
+ * Displays a user's chat message in a styled bubble.
15
+ * Used alongside AgentResponse to create chat interfaces.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * <UserPrompt
20
+ * content="What is the weather today?"
21
+ * timestamp={new Date()}
22
+ * />
23
+ * ```
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * // Custom styling
28
+ * <UserPrompt
29
+ * content="Hello world"
30
+ * className="max-w-full"
31
+ * />
32
+ * ```
33
+ */
34
+ export const UserPrompt = React.forwardRef<HTMLDivElement, UserPromptProps>(
35
+ ({ content, timestamp, className, ...props }, ref) => {
36
+ return (
37
+ <div
38
+ ref={ref}
39
+ className={cn(
40
+ "w-fit max-w-[80%] rounded-lg px-4 pt-3.5 pb-3",
41
+ "bg-secondary text-secondary-foreground",
42
+ className
43
+ )}
44
+ {...props}
45
+ >
46
+ <p className="whitespace-pre-wrap">{content}</p>
47
+ {timestamp && (
48
+ <p className="text-xs text-secondary-foreground/70 mt-1">
49
+ {timestamp.toLocaleTimeString([], {
50
+ hour: "2-digit",
51
+ minute: "2-digit",
52
+ })}
53
+ </p>
54
+ )}
55
+ </div>
56
+ );
57
+ }
58
+ );
59
+
60
+ UserPrompt.displayName = "UserPrompt";
@@ -0,0 +1 @@
1
+ export { UserPrompt, type UserPromptProps } from "./UserPrompt";
@@ -0,0 +1,265 @@
1
+ import * as React from "react";
2
+ import { Send, Loader2 } from "lucide-react";
3
+ import { cn, IconButton } from "@optilogic/core";
4
+ import {
5
+ SlateEditor,
6
+ Text,
7
+ type SlateEditorRef,
8
+ type NodeEntry,
9
+ type DecoratedRange,
10
+ type RenderLeafProps,
11
+ } from "@optilogic/editor";
12
+ import type { UserPromptInputProps, UserPromptInputRef } from "./types";
13
+
14
+ /**
15
+ * Creates a decorate function that highlights code blocks.
16
+ * Handles both complete (```...```) and unclosed (```...) code blocks.
17
+ */
18
+ function createCodeBlockDecorate(entry: NodeEntry): DecoratedRange[] {
19
+ const [node, path] = entry;
20
+ const ranges: DecoratedRange[] = [];
21
+
22
+ if (!Text.isText(node)) {
23
+ return ranges;
24
+ }
25
+
26
+ const { text } = node;
27
+
28
+ // Find all ``` positions
29
+ const backtickPositions: number[] = [];
30
+ let searchStart = 0;
31
+ while (true) {
32
+ const pos = text.indexOf("```", searchStart);
33
+ if (pos === -1) break;
34
+ backtickPositions.push(pos);
35
+ searchStart = pos + 3;
36
+ }
37
+
38
+ // Process pairs of backticks (and handle unclosed blocks)
39
+ let i = 0;
40
+ while (i < backtickPositions.length) {
41
+ const openPos = backtickPositions[i];
42
+ const closePos = backtickPositions[i + 1];
43
+
44
+ // Mark opening delimiter
45
+ ranges.push({
46
+ anchor: { path, offset: openPos },
47
+ focus: { path, offset: openPos + 3 },
48
+ codeDelimiter: true,
49
+ } as DecoratedRange);
50
+
51
+ if (closePos !== undefined) {
52
+ // Complete code block - mark content and closing delimiter
53
+ if (closePos > openPos + 3) {
54
+ ranges.push({
55
+ anchor: { path, offset: openPos + 3 },
56
+ focus: { path, offset: closePos },
57
+ codeBlock: true,
58
+ } as DecoratedRange);
59
+ }
60
+ ranges.push({
61
+ anchor: { path, offset: closePos },
62
+ focus: { path, offset: closePos + 3 },
63
+ codeDelimiter: true,
64
+ } as DecoratedRange);
65
+ i += 2; // Move past both opening and closing
66
+ } else {
67
+ // Unclosed code block - mark everything to end as code
68
+ if (text.length > openPos + 3) {
69
+ ranges.push({
70
+ anchor: { path, offset: openPos + 3 },
71
+ focus: { path, offset: text.length },
72
+ codeBlock: true,
73
+ } as DecoratedRange);
74
+ }
75
+ i += 1; // Move past opening only
76
+ }
77
+ }
78
+
79
+ return ranges;
80
+ }
81
+
82
+ /**
83
+ * Custom leaf renderer for code block styling
84
+ */
85
+ function CodeBlockLeaf({ attributes, children, leaf }: RenderLeafProps) {
86
+ const leafAny = leaf as { codeBlock?: boolean; codeDelimiter?: boolean };
87
+
88
+ if (leafAny.codeBlock) {
89
+ return (
90
+ <span
91
+ {...attributes}
92
+ className="bg-muted/50 text-muted-foreground font-mono text-sm rounded px-1"
93
+ >
94
+ {children}
95
+ </span>
96
+ );
97
+ }
98
+
99
+ if (leafAny.codeDelimiter) {
100
+ return (
101
+ <span
102
+ {...attributes}
103
+ className="text-muted-foreground/50 font-mono text-sm"
104
+ >
105
+ {children}
106
+ </span>
107
+ );
108
+ }
109
+
110
+ return <span {...attributes}>{children}</span>;
111
+ }
112
+
113
+ /**
114
+ * UserPromptInput Component
115
+ *
116
+ * A rich text input for user messages that wraps SlateEditor.
117
+ * Features:
118
+ * - Code block styling with triple backticks
119
+ * - Send button with loading state
120
+ * - Action slot for additional buttons
121
+ * - Tag support (optional)
122
+ *
123
+ * @example
124
+ * <UserPromptInput
125
+ * placeholder="Type your message..."
126
+ * onSubmit={(text) => sendMessage(text)}
127
+ * renderActions={() => (
128
+ * <IconButton icon={<Paperclip />} aria-label="Attach file" />
129
+ * )}
130
+ * />
131
+ */
132
+ export const UserPromptInput = React.forwardRef<
133
+ UserPromptInputRef,
134
+ UserPromptInputProps
135
+ >(
136
+ (
137
+ {
138
+ value = "",
139
+ onChange,
140
+ onSubmit,
141
+ clearOnSubmit = true,
142
+ placeholder = "Type your message...",
143
+ disabled = false,
144
+ isSubmitting = false,
145
+ minRows = 1,
146
+ maxRows = 6,
147
+ renderActions,
148
+ enableTags = false,
149
+ onTagCreate,
150
+ onTagDelete,
151
+ className,
152
+ ...props
153
+ },
154
+ ref
155
+ ) => {
156
+ const editorRef = React.useRef<SlateEditorRef>(null);
157
+ const [internalValue, setInternalValue] = React.useState(value);
158
+
159
+ // Sync internal value with prop
160
+ React.useEffect(() => {
161
+ setInternalValue(value);
162
+ }, [value]);
163
+
164
+ // Expose ref methods
165
+ React.useImperativeHandle(
166
+ ref,
167
+ () => ({
168
+ focus: () => editorRef.current?.focus(),
169
+ clear: () => {
170
+ editorRef.current?.clear();
171
+ setInternalValue("");
172
+ },
173
+ getText: () => editorRef.current?.getText() ?? "",
174
+ insertText: (text: string) => editorRef.current?.insertText(text),
175
+ }),
176
+ []
177
+ );
178
+
179
+ const handleChange = React.useCallback(
180
+ (newValue: string) => {
181
+ setInternalValue(newValue);
182
+ onChange?.(newValue);
183
+ },
184
+ [onChange]
185
+ );
186
+
187
+ const handleSubmit = React.useCallback(
188
+ (text: string) => {
189
+ if (disabled || isSubmitting) return;
190
+ if (!text.trim()) return;
191
+
192
+ onSubmit?.(text.trim());
193
+
194
+ if (clearOnSubmit) {
195
+ editorRef.current?.clear();
196
+ setInternalValue("");
197
+ }
198
+ },
199
+ [disabled, isSubmitting, onSubmit, clearOnSubmit]
200
+ );
201
+
202
+ const handleSendClick = React.useCallback(() => {
203
+ const text = editorRef.current?.getText() ?? "";
204
+ handleSubmit(text);
205
+ }, [handleSubmit]);
206
+
207
+ const hasContent = internalValue.trim().length > 0;
208
+ const canSubmit = hasContent && !disabled && !isSubmitting;
209
+
210
+ return (
211
+ <div
212
+ className={cn(
213
+ "rounded-lg border border-input bg-background",
214
+ disabled && "opacity-50 cursor-not-allowed",
215
+ className
216
+ )}
217
+ {...props}
218
+ >
219
+ {/* Editor area */}
220
+ <div className="pl-2 pr-0 pt-1 pb-1">
221
+ <SlateEditor
222
+ ref={editorRef}
223
+ value={internalValue}
224
+ onChange={handleChange}
225
+ onSubmit={handleSubmit}
226
+ clearOnSubmit={false}
227
+ placeholder={placeholder}
228
+ disabled={disabled || isSubmitting}
229
+ enableTags={enableTags}
230
+ onTagCreate={onTagCreate}
231
+ onTagDelete={onTagDelete}
232
+ minRows={minRows}
233
+ maxRows={maxRows}
234
+ decorate={createCodeBlockDecorate}
235
+ renderLeaf={CodeBlockLeaf}
236
+ />
237
+ </div>
238
+
239
+ {/* Actions row */}
240
+ <div className="flex items-center justify-between pl-2 pr-1 pb-1 pt-1">
241
+ {/* Left actions slot */}
242
+ <div className="flex items-center gap-1">{renderActions?.()}</div>
243
+
244
+ {/* Send button */}
245
+ <IconButton
246
+ icon={
247
+ isSubmitting ? (
248
+ <Loader2 className="animate-spin" />
249
+ ) : (
250
+ <Send />
251
+ )
252
+ }
253
+ variant="filled"
254
+ size="sm"
255
+ aria-label={isSubmitting ? "Sending..." : "Send message"}
256
+ disabled={!canSubmit}
257
+ onClick={handleSendClick}
258
+ />
259
+ </div>
260
+ </div>
261
+ );
262
+ }
263
+ );
264
+
265
+ UserPromptInput.displayName = "UserPromptInput";
@@ -0,0 +1,2 @@
1
+ export { UserPromptInput } from "./UserPromptInput";
2
+ export type { UserPromptInputProps, UserPromptInputRef } from "./types";
@@ -0,0 +1,42 @@
1
+ import * as React from "react";
2
+
3
+ export interface UserPromptInputProps
4
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange" | "onSubmit"> {
5
+ /** Current text value */
6
+ value?: string;
7
+ /** Callback when text changes */
8
+ onChange?: (value: string) => void;
9
+ /** Callback when user submits (Enter key or send button) */
10
+ onSubmit?: (text: string) => void;
11
+ /** Clear input after submit (default: true) */
12
+ clearOnSubmit?: boolean;
13
+ /** Placeholder text */
14
+ placeholder?: string;
15
+ /** Whether the input is disabled */
16
+ disabled?: boolean;
17
+ /** Whether a submission is in progress (shows loading state) */
18
+ isSubmitting?: boolean;
19
+ /** Minimum number of rows for the editor */
20
+ minRows?: number;
21
+ /** Maximum number of rows before scrolling */
22
+ maxRows?: number;
23
+ /** Render function for additional action buttons (left side) */
24
+ renderActions?: () => React.ReactNode;
25
+ /** Enable tag detection with # character */
26
+ enableTags?: boolean;
27
+ /** Callback when a tag is created */
28
+ onTagCreate?: (tag: string) => void;
29
+ /** Callback when a tag is deleted */
30
+ onTagDelete?: (tag: string) => void;
31
+ }
32
+
33
+ export interface UserPromptInputRef {
34
+ /** Focus the editor */
35
+ focus: () => void;
36
+ /** Clear the editor content */
37
+ clear: () => void;
38
+ /** Get the current text content */
39
+ getText: () => string;
40
+ /** Insert text at cursor position */
41
+ insertText: (text: string) => void;
42
+ }
package/src/index.ts CHANGED
@@ -44,3 +44,13 @@ export {
44
44
  formatTime,
45
45
  formatTotalTime,
46
46
  } from "./components/agent-response";
47
+
48
+ // User Prompt - Component for displaying user messages
49
+ export { UserPrompt, type UserPromptProps } from "./components/user-prompt";
50
+
51
+ // User Prompt Input - Component for user message input
52
+ export {
53
+ UserPromptInput,
54
+ type UserPromptInputProps,
55
+ type UserPromptInputRef,
56
+ } from "./components/user-prompt-input";