@optilogic/chat 1.0.0-beta.1

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.
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@optilogic/chat",
3
+ "version": "1.0.0-beta.1",
4
+ "description": "Chat UI components for Optilogic - AgentResponse and related components for LLM interactions",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "src",
24
+ "README.md"
25
+ ],
26
+ "dependencies": {
27
+ "@optilogic/core": "1.0.0-beta.1"
28
+ },
29
+ "peerDependencies": {
30
+ "lucide-react": "^0.400.0",
31
+ "react": "^18.0.0 || ^19.0.0",
32
+ "react-dom": "^18.0.0 || ^19.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/react": "^18.3.0",
36
+ "@types/react-dom": "^18.3.0",
37
+ "lucide-react": "^0.468.0",
38
+ "react": "^18.3.1",
39
+ "react-dom": "^18.3.1",
40
+ "tsup": "^8.3.5",
41
+ "typescript": "^5.7.2"
42
+ },
43
+ "keywords": [
44
+ "react",
45
+ "chat",
46
+ "llm",
47
+ "ai",
48
+ "agent",
49
+ "optilogic"
50
+ ],
51
+ "license": "MIT",
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "https://github.com/optilogic/opti-ui"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "scripts": {
60
+ "build": "tsup",
61
+ "dev": "tsup --watch",
62
+ "typecheck": "tsc --noEmit",
63
+ "lint": "eslint src --ext .ts,.tsx",
64
+ "clean": "rm -rf dist .turbo"
65
+ }
66
+ }
@@ -0,0 +1,232 @@
1
+ /**
2
+ * AgentResponse Component
3
+ *
4
+ * A library-ready presentational component for displaying AI agent responses.
5
+ * Displays thinking, tool calls, knowledge retrieval, memory access, and final response.
6
+ */
7
+
8
+ import * as React from "react";
9
+ import { useState, useMemo, useCallback } from "react";
10
+ import { cn } from "@optilogic/core";
11
+ import { MetadataRow, ThinkingSection, ActionBar } from "./components";
12
+ import { useThinkingTimer } from "./hooks";
13
+ import type { AgentResponseState, FeedbackValue } from "./types";
14
+
15
+ export interface AgentResponseProps extends React.HTMLAttributes<HTMLDivElement> {
16
+ /** The response state to render */
17
+ state: AgentResponseState;
18
+
19
+ /** Optional unique ID (for list keys) */
20
+ id?: string;
21
+
22
+ /** Optional timestamp to display */
23
+ timestamp?: Date;
24
+
25
+ /** Feedback state (controlled) */
26
+ feedback?: FeedbackValue;
27
+ onFeedbackChange?: (feedback: FeedbackValue) => void;
28
+
29
+ /** Callback when the response is copied via the action bar */
30
+ onResponseCopy?: (response: string) => void;
31
+
32
+ /** Thinking section expansion (controlled or uncontrolled) */
33
+ defaultThinkingExpanded?: boolean;
34
+ thinkingExpanded?: boolean;
35
+ onThinkingExpandedChange?: (expanded: boolean) => void;
36
+
37
+ /** Action bar visibility mode */
38
+ actionsVisible?: boolean | "hover";
39
+
40
+ /**
41
+ * Custom markdown renderer for the response content.
42
+ * If not provided, the response will be rendered as plain text.
43
+ *
44
+ * @example
45
+ * <AgentResponse
46
+ * state={state}
47
+ * renderMarkdown={(content) => <MyMarkdownRenderer content={content} />}
48
+ * />
49
+ */
50
+ renderMarkdown?: (content: string) => React.ReactNode;
51
+ }
52
+
53
+ /**
54
+ * AgentResponse Component
55
+ *
56
+ * A complete component for displaying AI agent responses including:
57
+ * - Thinking/reasoning content (collapsible)
58
+ * - Tool calls, knowledge retrieval, and memory access indicators
59
+ * - Final response with optional markdown rendering
60
+ * - Action bar with copy and feedback buttons
61
+ *
62
+ * @example
63
+ * // Basic usage with useAgentResponseAccumulator hook
64
+ * const { state, handleMessage } = useAgentResponseAccumulator();
65
+ *
66
+ * <AgentResponse state={state} />
67
+ *
68
+ * @example
69
+ * // With markdown rendering and feedback
70
+ * <AgentResponse
71
+ * state={state}
72
+ * renderMarkdown={(content) => <ReactMarkdown>{content}</ReactMarkdown>}
73
+ * feedback={feedback}
74
+ * onFeedbackChange={setFeedback}
75
+ * />
76
+ *
77
+ * @example
78
+ * // Controlled thinking expansion
79
+ * <AgentResponse
80
+ * state={state}
81
+ * thinkingExpanded={isExpanded}
82
+ * onThinkingExpandedChange={setIsExpanded}
83
+ * />
84
+ */
85
+ const AgentResponse = React.forwardRef<HTMLDivElement, AgentResponseProps>(
86
+ (
87
+ {
88
+ state,
89
+ id,
90
+ timestamp,
91
+ feedback,
92
+ onFeedbackChange,
93
+ onResponseCopy,
94
+ defaultThinkingExpanded = false,
95
+ thinkingExpanded: controlledThinkingExpanded,
96
+ onThinkingExpandedChange,
97
+ actionsVisible = "hover",
98
+ renderMarkdown,
99
+ className,
100
+ ...props
101
+ },
102
+ ref
103
+ ) => {
104
+ // Uncontrolled thinking expanded state
105
+ const [uncontrolledExpanded, setUncontrolledExpanded] = useState(defaultThinkingExpanded);
106
+
107
+ // Determine if thinking is controlled
108
+ const isThinkingControlled = controlledThinkingExpanded !== undefined;
109
+ const thinkingExpanded = isThinkingControlled
110
+ ? controlledThinkingExpanded
111
+ : uncontrolledExpanded;
112
+
113
+ // Toggle thinking handler
114
+ const toggleThinking = useCallback(() => {
115
+ const newValue = !thinkingExpanded;
116
+ if (isThinkingControlled) {
117
+ onThinkingExpandedChange?.(newValue);
118
+ } else {
119
+ setUncontrolledExpanded(newValue);
120
+ }
121
+ }, [thinkingExpanded, isThinkingControlled, onThinkingExpandedChange]);
122
+
123
+ // Hover state for action bar visibility
124
+ const [isHovered, setIsHovered] = useState(false);
125
+
126
+ // Thinking timer
127
+ const elapsedTime = useThinkingTimer({
128
+ startTime: state.thinkingStartTime,
129
+ endTime: state.responseCompleteTime,
130
+ status: state.status,
131
+ });
132
+
133
+ // Calculate total time (from first message to response complete)
134
+ const totalTimeSeconds = useMemo(() => {
135
+ if (!state.firstMessageTime || !state.responseCompleteTime) return 0;
136
+ return (state.responseCompleteTime - state.firstMessageTime) / 1000;
137
+ }, [state.firstMessageTime, state.responseCompleteTime]);
138
+
139
+ // Derived state: has any content been received?
140
+ const hasAnyContent =
141
+ state.thinking ||
142
+ state.toolCalls.length > 0 ||
143
+ state.knowledge.length > 0 ||
144
+ state.memory.length > 0 ||
145
+ state.response;
146
+
147
+ // Derived state: should show metadata row?
148
+ const showMetadataRow =
149
+ state.thinking ||
150
+ state.toolCalls.length > 0 ||
151
+ state.knowledge.length > 0 ||
152
+ state.memory.length > 0 ||
153
+ state.status === "processing";
154
+
155
+ // Determine action bar visibility
156
+ const showActionBar = state.status === "complete" && state.response;
157
+ const isActionBarVisible =
158
+ actionsVisible === true ||
159
+ (actionsVisible === "hover" && isHovered);
160
+
161
+ // If no content, render nothing
162
+ if (!hasAnyContent) {
163
+ return null;
164
+ }
165
+
166
+ return (
167
+ <div
168
+ ref={ref}
169
+ className={className}
170
+ onMouseEnter={() => setIsHovered(true)}
171
+ onMouseLeave={() => setIsHovered(false)}
172
+ {...props}
173
+ >
174
+ {/* Message Content Container */}
175
+ <div className="border border-border rounded-lg overflow-hidden">
176
+ {/* Metadata Row - show if there's any metadata or thinking */}
177
+ {showMetadataRow && (
178
+ <>
179
+ <MetadataRow
180
+ hasThinking={!!state.thinking}
181
+ isExpanded={thinkingExpanded}
182
+ onToggle={toggleThinking}
183
+ toolCalls={state.toolCalls}
184
+ knowledge={state.knowledge}
185
+ memory={state.memory}
186
+ status={state.status}
187
+ elapsedTime={elapsedTime}
188
+ />
189
+
190
+ {/* Thinking Content - collapsible with max-height */}
191
+ <ThinkingSection
192
+ content={state.thinking}
193
+ isExpanded={thinkingExpanded}
194
+ />
195
+ </>
196
+ )}
197
+
198
+ {/* Response Section */}
199
+ {state.response && (
200
+ <div
201
+ className={cn(
202
+ "bg-muted/50 p-4",
203
+ showMetadataRow && "border-t border-border"
204
+ )}
205
+ >
206
+ {renderMarkdown ? (
207
+ renderMarkdown(state.response)
208
+ ) : (
209
+ <span className="whitespace-pre-wrap">{state.response}</span>
210
+ )}
211
+ </div>
212
+ )}
213
+ </div>
214
+
215
+ {/* Action Bar - outside the message container, visible on hover when complete */}
216
+ {showActionBar && (
217
+ <ActionBar
218
+ response={state.response}
219
+ isVisible={isActionBarVisible}
220
+ totalTimeSeconds={totalTimeSeconds}
221
+ feedback={feedback}
222
+ onFeedbackChange={onFeedbackChange}
223
+ onResponseCopy={onResponseCopy}
224
+ />
225
+ )}
226
+ </div>
227
+ );
228
+ }
229
+ );
230
+ AgentResponse.displayName = "AgentResponse";
231
+
232
+ export { AgentResponse };
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Action Bar Component
3
+ *
4
+ * Displays copy, feedback, and timing actions for the response
5
+ */
6
+
7
+ import * as React from "react";
8
+ import { useState, useCallback } from "react";
9
+ import { Copy, Check, ThumbsUp, ThumbsDown } from "lucide-react";
10
+ import { cn } from "@optilogic/core";
11
+ import { formatTotalTime } from "../utils";
12
+ import type { FeedbackValue } from "../types";
13
+
14
+ export interface ActionBarProps extends React.HTMLAttributes<HTMLDivElement> {
15
+ /** The response text (for copying) */
16
+ response: string;
17
+ /** Whether the action bar is visible */
18
+ isVisible: boolean;
19
+ /** Total time in seconds */
20
+ totalTimeSeconds: number;
21
+ /** Current feedback value */
22
+ feedback?: FeedbackValue;
23
+ /** Callback when feedback changes */
24
+ onFeedbackChange?: (feedback: FeedbackValue) => void;
25
+ /** Callback when response is copied */
26
+ onResponseCopy?: (response: string) => void;
27
+ }
28
+
29
+ /**
30
+ * ActionBar Component
31
+ *
32
+ * Displays action buttons for copying the response, providing feedback,
33
+ * and showing total response time.
34
+ *
35
+ * @example
36
+ * <ActionBar
37
+ * response={state.response}
38
+ * isVisible={isHovered}
39
+ * totalTimeSeconds={totalTime}
40
+ * feedback={feedback}
41
+ * onFeedbackChange={setFeedback}
42
+ * onCopy={handleCopy}
43
+ * />
44
+ */
45
+ const ActionBar = React.forwardRef<HTMLDivElement, ActionBarProps>(
46
+ (
47
+ {
48
+ response,
49
+ isVisible,
50
+ totalTimeSeconds,
51
+ feedback,
52
+ onFeedbackChange,
53
+ onResponseCopy,
54
+ className,
55
+ ...props
56
+ },
57
+ ref
58
+ ) => {
59
+ const [copied, setCopied] = useState(false);
60
+
61
+ const handleCopy = useCallback(async () => {
62
+ try {
63
+ await navigator.clipboard.writeText(response);
64
+ setCopied(true);
65
+ setTimeout(() => setCopied(false), 2000);
66
+ onResponseCopy?.(response);
67
+ } catch (err) {
68
+ console.error("Failed to copy response:", err);
69
+ }
70
+ }, [response, onResponseCopy]);
71
+
72
+ const handleThumbsUp = useCallback(() => {
73
+ const newValue = feedback === "up" ? null : "up";
74
+ onFeedbackChange?.(newValue);
75
+ }, [feedback, onFeedbackChange]);
76
+
77
+ const handleThumbsDown = useCallback(() => {
78
+ const newValue = feedback === "down" ? null : "down";
79
+ onFeedbackChange?.(newValue);
80
+ }, [feedback, onFeedbackChange]);
81
+
82
+ const isThumbsUp = feedback === "up";
83
+ const isThumbsDown = feedback === "down";
84
+
85
+ return (
86
+ <div
87
+ ref={ref}
88
+ className={cn(
89
+ "flex items-center justify-between px-4 py-2",
90
+ "transition-opacity duration-200",
91
+ isVisible ? "opacity-100" : "opacity-0 pointer-events-none",
92
+ className
93
+ )}
94
+ {...props}
95
+ >
96
+ {/* Left side - action buttons */}
97
+ <div className="flex items-center gap-1">
98
+ {/* Copy button */}
99
+ <button
100
+ onClick={handleCopy}
101
+ className="p-1.5 rounded hover:bg-muted transition-colors text-muted-foreground hover:text-foreground"
102
+ title={copied ? "Copied!" : "Copy response"}
103
+ >
104
+ {copied ? (
105
+ <Check className="w-4 h-4 text-green-500" />
106
+ ) : (
107
+ <Copy className="w-4 h-4" />
108
+ )}
109
+ </button>
110
+
111
+ {/* Thumbs up */}
112
+ <button
113
+ onClick={handleThumbsUp}
114
+ className={cn(
115
+ "p-1.5 rounded hover:bg-muted transition-colors",
116
+ isThumbsUp
117
+ ? "text-green-500"
118
+ : "text-muted-foreground hover:text-foreground"
119
+ )}
120
+ title="Good response"
121
+ >
122
+ <ThumbsUp className={cn("w-4 h-4", isThumbsUp && "fill-current")} />
123
+ </button>
124
+
125
+ {/* Thumbs down */}
126
+ <button
127
+ onClick={handleThumbsDown}
128
+ className={cn(
129
+ "p-1.5 rounded hover:bg-muted transition-colors",
130
+ isThumbsDown
131
+ ? "text-red-500"
132
+ : "text-muted-foreground hover:text-foreground"
133
+ )}
134
+ title="Poor response"
135
+ >
136
+ <ThumbsDown className={cn("w-4 h-4", isThumbsDown && "fill-current")} />
137
+ </button>
138
+ </div>
139
+
140
+ {/* Right side - timing info */}
141
+ <span className="text-xs text-muted-foreground">
142
+ Total time: {formatTotalTime(totalTimeSeconds)}
143
+ </span>
144
+ </div>
145
+ );
146
+ }
147
+ );
148
+ ActionBar.displayName = "ActionBar";
149
+
150
+ export { ActionBar };
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Activity Indicators Component
3
+ *
4
+ * Displays tool, knowledge, memory icons with counts and popovers
5
+ */
6
+
7
+ import * as React from "react";
8
+ import { Wrench, Book, HardDrive } from "lucide-react";
9
+ import { cn, Popover, PopoverTrigger, PopoverContent } from "@optilogic/core";
10
+ import type { ToolCall, KnowledgeItem, MemoryItem } from "../types";
11
+
12
+ export interface ActivityIndicatorsProps extends React.HTMLAttributes<HTMLDivElement> {
13
+ /** Tool calls to display */
14
+ toolCalls: ToolCall[];
15
+ /** Knowledge items to display */
16
+ knowledge: KnowledgeItem[];
17
+ /** Memory items to display */
18
+ memory: MemoryItem[];
19
+ }
20
+
21
+ /**
22
+ * ActivityIndicators Component
23
+ *
24
+ * Displays icons with counts for tool calls, knowledge retrieval, and memory access.
25
+ * Each icon has a popover showing details when clicked.
26
+ *
27
+ * @example
28
+ * <ActivityIndicators
29
+ * toolCalls={state.toolCalls}
30
+ * knowledge={state.knowledge}
31
+ * memory={state.memory}
32
+ * />
33
+ */
34
+ const ActivityIndicators = React.forwardRef<HTMLDivElement, ActivityIndicatorsProps>(
35
+ ({ toolCalls, knowledge, memory, className, ...props }, ref) => {
36
+ const hasAnyActivity =
37
+ toolCalls.length > 0 || knowledge.length > 0 || memory.length > 0;
38
+
39
+ if (!hasAnyActivity) return null;
40
+
41
+ return (
42
+ <div ref={ref} className={cn("flex items-center gap-2", className)} {...props}>
43
+ {/* Tool Calls */}
44
+ {toolCalls.length > 0 && (
45
+ <Popover>
46
+ <PopoverTrigger asChild>
47
+ <button
48
+ className="flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors"
49
+ onClick={(e) => e.stopPropagation()}
50
+ >
51
+ <Wrench className="w-3.5 h-3.5" />
52
+ <span className="text-xs">{toolCalls.length}</span>
53
+ </button>
54
+ </PopoverTrigger>
55
+ <PopoverContent className="w-80">
56
+ <div className="space-y-2">
57
+ <h4 className="font-medium text-sm">Tool Calls</h4>
58
+ <div className="space-y-2 max-h-60 overflow-auto">
59
+ {toolCalls.map((tool) => (
60
+ <div key={tool.id} className="p-2 bg-muted rounded text-xs">
61
+ <div className="font-medium">{tool.name}</div>
62
+ {tool.arguments && (
63
+ <pre className="mt-1 text-muted-foreground overflow-x-auto">
64
+ {JSON.stringify(tool.arguments, null, 2)}
65
+ </pre>
66
+ )}
67
+ </div>
68
+ ))}
69
+ </div>
70
+ </div>
71
+ </PopoverContent>
72
+ </Popover>
73
+ )}
74
+
75
+ {/* Knowledge */}
76
+ {knowledge.length > 0 && (
77
+ <Popover>
78
+ <PopoverTrigger asChild>
79
+ <button
80
+ className="flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors"
81
+ onClick={(e) => e.stopPropagation()}
82
+ >
83
+ <Book className="w-3.5 h-3.5" />
84
+ <span className="text-xs">{knowledge.length}</span>
85
+ </button>
86
+ </PopoverTrigger>
87
+ <PopoverContent className="w-80">
88
+ <div className="space-y-2">
89
+ <h4 className="font-medium text-sm">Knowledge Retrieved</h4>
90
+ <div className="space-y-2 max-h-60 overflow-auto">
91
+ {knowledge.map((item) => (
92
+ <div key={item.id} className="p-2 bg-muted rounded text-xs">
93
+ <div className="font-medium">{item.source}</div>
94
+ <div className="mt-1 text-muted-foreground">
95
+ {item.content}
96
+ </div>
97
+ </div>
98
+ ))}
99
+ </div>
100
+ </div>
101
+ </PopoverContent>
102
+ </Popover>
103
+ )}
104
+
105
+ {/* Memory */}
106
+ {memory.length > 0 && (
107
+ <Popover>
108
+ <PopoverTrigger asChild>
109
+ <button
110
+ className="flex items-center gap-1 text-muted-foreground hover:text-foreground transition-colors"
111
+ onClick={(e) => e.stopPropagation()}
112
+ >
113
+ <HardDrive className="w-3.5 h-3.5" />
114
+ <span className="text-xs">{memory.length}</span>
115
+ </button>
116
+ </PopoverTrigger>
117
+ <PopoverContent className="w-80">
118
+ <div className="space-y-2">
119
+ <h4 className="font-medium text-sm">Memory Accessed</h4>
120
+ <div className="space-y-2 max-h-60 overflow-auto">
121
+ {memory.map((item) => (
122
+ <div key={item.id} className="p-2 bg-muted rounded text-xs">
123
+ <div className="font-medium">{item.type}</div>
124
+ <div className="mt-1 text-muted-foreground">
125
+ {item.content}
126
+ </div>
127
+ </div>
128
+ ))}
129
+ </div>
130
+ </div>
131
+ </PopoverContent>
132
+ </Popover>
133
+ )}
134
+ </div>
135
+ );
136
+ }
137
+ );
138
+ ActivityIndicators.displayName = "ActivityIndicators";
139
+
140
+ export { ActivityIndicators };