@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.
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Metadata Row Component
3
+ *
4
+ * Displays thinking toggle, timer, and activity indicators
5
+ */
6
+
7
+ import * as React from "react";
8
+ import { ChevronDown, ChevronUp } from "lucide-react";
9
+ import { cn, LoadingSpinner } from "@optilogic/core";
10
+ import { ActivityIndicators } from "./ActivityIndicators";
11
+ import { formatTime } from "../utils";
12
+ import type { AgentResponseStatus, ToolCall, KnowledgeItem, MemoryItem } from "../types";
13
+
14
+ export interface MetadataRowProps extends React.HTMLAttributes<HTMLDivElement> {
15
+ /** Whether there is thinking content */
16
+ hasThinking: boolean;
17
+ /** Whether the thinking section is expanded */
18
+ isExpanded: boolean;
19
+ /** Toggle callback for thinking expansion */
20
+ onToggle: () => void;
21
+ /** Tool calls to display */
22
+ toolCalls: ToolCall[];
23
+ /** Knowledge items to display */
24
+ knowledge: KnowledgeItem[];
25
+ /** Memory items to display */
26
+ memory: MemoryItem[];
27
+ /** Current response status */
28
+ status: AgentResponseStatus;
29
+ /** Elapsed time in seconds */
30
+ elapsedTime: number;
31
+ }
32
+
33
+ /**
34
+ * MetadataRow Component
35
+ *
36
+ * Displays the metadata row with thinking toggle, timer, and activity indicators.
37
+ * When thinking content is present, the row is clickable to toggle expansion.
38
+ *
39
+ * @example
40
+ * <MetadataRow
41
+ * hasThinking={!!state.thinking}
42
+ * isExpanded={thinkingExpanded}
43
+ * onToggle={toggleThinking}
44
+ * toolCalls={state.toolCalls}
45
+ * knowledge={state.knowledge}
46
+ * memory={state.memory}
47
+ * status={state.status}
48
+ * elapsedTime={elapsedTime}
49
+ * />
50
+ */
51
+ const MetadataRow = React.forwardRef<HTMLDivElement, MetadataRowProps>(
52
+ (
53
+ {
54
+ hasThinking,
55
+ isExpanded,
56
+ onToggle,
57
+ toolCalls,
58
+ knowledge,
59
+ memory,
60
+ status,
61
+ elapsedTime,
62
+ className,
63
+ ...props
64
+ },
65
+ ref
66
+ ) => {
67
+ const isProcessing = status === "processing";
68
+ const isComplete = status === "complete";
69
+ const hasActivity =
70
+ toolCalls.length > 0 || knowledge.length > 0 || memory.length > 0;
71
+
72
+ // Determine what to show on the left side
73
+ const renderLeftContent = () => {
74
+ // If we have thinking text, show collapse toggle + label + timer
75
+ if (hasThinking) {
76
+ return (
77
+ <div className="flex items-center gap-1.5">
78
+ {isExpanded ? (
79
+ <ChevronUp className="w-3.5 h-3.5 text-muted-foreground" />
80
+ ) : (
81
+ <ChevronDown className="w-3.5 h-3.5 text-muted-foreground" />
82
+ )}
83
+ <span className="text-xs text-muted-foreground">
84
+ {isComplete
85
+ ? `Thought for ${formatTime(elapsedTime, true)}`
86
+ : `Thinking... ${formatTime(elapsedTime, false)}`}
87
+ </span>
88
+ </div>
89
+ );
90
+ }
91
+
92
+ // If processing but no thinking text yet, show spinner
93
+ if (isProcessing) {
94
+ return (
95
+ <div className="flex items-center gap-1.5">
96
+ <LoadingSpinner size="sm" variant="muted" className="w-3.5 h-3.5" />
97
+ <span className="text-xs text-muted-foreground">Processing</span>
98
+ </div>
99
+ );
100
+ }
101
+
102
+ // Complete with no thinking - show nothing on left (just activity on right)
103
+ return null;
104
+ };
105
+
106
+ const leftContent = renderLeftContent();
107
+
108
+ // If nothing to show (no thinking, not processing, no activity), hide the row
109
+ if (!leftContent && !hasActivity) {
110
+ return null;
111
+ }
112
+
113
+ // Always use a div for the row to avoid nesting buttons
114
+ // When there's thinking, only the left side is clickable
115
+ return (
116
+ <div
117
+ ref={ref}
118
+ className={cn("w-full flex items-center justify-between px-3 py-2", className)}
119
+ {...props}
120
+ >
121
+ {/* Left content - clickable when there's thinking */}
122
+ {hasThinking ? (
123
+ <button
124
+ onClick={onToggle}
125
+ className="flex items-center gap-1.5 hover:bg-muted/50 -ml-1.5 pl-1.5 pr-2 py-0.5 rounded transition-colors"
126
+ >
127
+ {leftContent}
128
+ </button>
129
+ ) : (
130
+ <div className="flex items-center gap-1.5">
131
+ {leftContent}
132
+ </div>
133
+ )}
134
+ <ActivityIndicators
135
+ toolCalls={toolCalls}
136
+ knowledge={knowledge}
137
+ memory={memory}
138
+ />
139
+ </div>
140
+ );
141
+ }
142
+ );
143
+ MetadataRow.displayName = "MetadataRow";
144
+
145
+ export { MetadataRow };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Thinking Section Component
3
+ *
4
+ * Collapsible section for displaying agent thinking/reasoning content
5
+ */
6
+
7
+ import * as React from "react";
8
+ import { cn } from "@optilogic/core";
9
+
10
+ export interface ThinkingSectionProps extends React.HTMLAttributes<HTMLDivElement> {
11
+ /** The thinking content to display */
12
+ content: string;
13
+ /** Whether the section is expanded */
14
+ isExpanded: boolean;
15
+ }
16
+
17
+ /**
18
+ * ThinkingSection Component
19
+ *
20
+ * Displays the agent's thinking/reasoning content in a collapsible panel.
21
+ *
22
+ * @example
23
+ * <ThinkingSection content={state.thinking} isExpanded={isExpanded} />
24
+ */
25
+ const ThinkingSection = React.forwardRef<HTMLDivElement, ThinkingSectionProps>(
26
+ ({ content, isExpanded, className, ...props }, ref) => {
27
+ if (!isExpanded || !content) {
28
+ return null;
29
+ }
30
+
31
+ return (
32
+ <div
33
+ ref={ref}
34
+ className={cn("px-3 pb-3 border-t border-border", className)}
35
+ {...props}
36
+ >
37
+ <div className="mt-2 max-h-[200px] overflow-y-auto">
38
+ <pre className="text-xs text-muted-foreground whitespace-pre-wrap font-mono">
39
+ {content}
40
+ </pre>
41
+ </div>
42
+ </div>
43
+ );
44
+ }
45
+ );
46
+ ThinkingSection.displayName = "ThinkingSection";
47
+
48
+ export { ThinkingSection };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Agent Response Sub-Components
3
+ */
4
+
5
+ export { ActivityIndicators } from "./ActivityIndicators";
6
+ export type { ActivityIndicatorsProps } from "./ActivityIndicators";
7
+
8
+ export { MetadataRow } from "./MetadataRow";
9
+ export type { MetadataRowProps } from "./MetadataRow";
10
+
11
+ export { ThinkingSection } from "./ThinkingSection";
12
+ export type { ThinkingSectionProps } from "./ThinkingSection";
13
+
14
+ export { ActionBar } from "./ActionBar";
15
+ export type { ActionBarProps } from "./ActionBar";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Agent Response Hooks
3
+ */
4
+
5
+ export { useThinkingTimer } from "./useThinkingTimer";
6
+ export type { UseThinkingTimerOptions } from "./useThinkingTimer";
7
+
8
+ export { useAgentResponseAccumulator } from "./useAgentResponseAccumulator";
9
+ export type {
10
+ UseAgentResponseAccumulatorOptions,
11
+ UseAgentResponseAccumulatorReturn,
12
+ } from "./useAgentResponseAccumulator";
@@ -0,0 +1,186 @@
1
+ /**
2
+ * useAgentResponseAccumulator Hook
3
+ *
4
+ * Accumulates agent response messages into a unified state
5
+ */
6
+
7
+ import { useState, useCallback } from "react";
8
+ import {
9
+ initialAgentResponseState,
10
+ type AgentResponseState,
11
+ type AgentMessage,
12
+ type GenericWebSocketMessage,
13
+ type ToolCall,
14
+ type KnowledgeItem,
15
+ type MemoryItem,
16
+ } from "../types";
17
+
18
+ export interface UseAgentResponseAccumulatorOptions {
19
+ /** WebSocket topic to filter messages (optional, for convenience) */
20
+ topic?: string;
21
+ }
22
+
23
+ export interface UseAgentResponseAccumulatorReturn {
24
+ /** Current accumulated state */
25
+ state: AgentResponseState;
26
+
27
+ /** Handler to process incoming messages */
28
+ handleMessage: (message: unknown) => void;
29
+
30
+ /** Reset state to initial */
31
+ reset: () => void;
32
+ }
33
+
34
+ /**
35
+ * Hook for accumulating agent response messages into a unified state
36
+ *
37
+ * @example
38
+ * const { state, handleMessage, reset } = useAgentResponseAccumulator({ topic: "agent" });
39
+ *
40
+ * // In your WebSocket handler:
41
+ * websocket.onmessage = (event) => {
42
+ * handleMessage(JSON.parse(event.data));
43
+ * };
44
+ */
45
+ export function useAgentResponseAccumulator(
46
+ options?: UseAgentResponseAccumulatorOptions
47
+ ): UseAgentResponseAccumulatorReturn {
48
+ const [state, setState] = useState<AgentResponseState>(initialAgentResponseState);
49
+ const topic = options?.topic;
50
+
51
+ const handleMessage = useCallback(
52
+ (message: unknown) => {
53
+ // If topic filter is provided, check for matching topic
54
+ let payload: AgentMessage;
55
+
56
+ if (topic) {
57
+ const msg = message as GenericWebSocketMessage;
58
+ if (msg.topic !== topic) return;
59
+ payload = msg.message;
60
+ } else {
61
+ // Assume message is the payload directly
62
+ payload = message as AgentMessage;
63
+ }
64
+
65
+ setState((prev) => {
66
+ // If we receive a non-status message while idle, transition to processing
67
+ let newStatus = prev.status;
68
+ const isFirstMessage = prev.status === "idle" && payload.type !== "status";
69
+ if (isFirstMessage) {
70
+ newStatus = "processing";
71
+ }
72
+
73
+ // Track first message time for total time calculation
74
+ const firstMessageTime =
75
+ prev.firstMessageTime ?? (isFirstMessage ? Date.now() : null);
76
+
77
+ switch (payload.type) {
78
+ case "status":
79
+ // "Harness connected" resets to idle
80
+ if (
81
+ payload.message === "Harness connected" ||
82
+ payload.status === "Harness connected"
83
+ ) {
84
+ return { ...initialAgentResponseState };
85
+ }
86
+ return { ...prev, status: newStatus };
87
+
88
+ case "thinking": {
89
+ const newThinking = payload.message || payload.content || "";
90
+ // Add line break between thinking messages
91
+ const separator = prev.thinking && newThinking ? "\n\n" : "";
92
+ // Set thinkingStartTime on first thinking message
93
+ const thinkingStartTime =
94
+ prev.thinkingStartTime ?? (newThinking ? Date.now() : null);
95
+ return {
96
+ ...prev,
97
+ status: newStatus,
98
+ thinking: prev.thinking + separator + newThinking,
99
+ thinkingStartTime,
100
+ firstMessageTime,
101
+ };
102
+ }
103
+
104
+ case "tool_call": {
105
+ // Handle both formats: { message: "ToolName" } or { tool: { id, name, arguments } }
106
+ const toolName = payload.message || payload.tool?.name;
107
+ if (toolName) {
108
+ const newToolCall: ToolCall = {
109
+ id: payload.tool?.id || `tool-${Date.now()}`,
110
+ name: toolName,
111
+ arguments: payload.tool?.arguments,
112
+ timestamp: Date.now(),
113
+ };
114
+ return {
115
+ ...prev,
116
+ status: newStatus,
117
+ toolCalls: [...prev.toolCalls, newToolCall],
118
+ firstMessageTime,
119
+ };
120
+ }
121
+ return { ...prev, status: newStatus, firstMessageTime };
122
+ }
123
+
124
+ case "knowledge": {
125
+ // Handle both formats: { message: "content" } or { knowledge: { id, source, content } }
126
+ const knowledgeContent = payload.message || payload.knowledge?.content;
127
+ if (knowledgeContent) {
128
+ const newKnowledge: KnowledgeItem = {
129
+ id: payload.knowledge?.id || `knowledge-${Date.now()}`,
130
+ source: payload.knowledge?.source || "unknown",
131
+ content: knowledgeContent,
132
+ timestamp: Date.now(),
133
+ };
134
+ return {
135
+ ...prev,
136
+ status: newStatus,
137
+ knowledge: [...prev.knowledge, newKnowledge],
138
+ firstMessageTime,
139
+ };
140
+ }
141
+ return { ...prev, status: newStatus, firstMessageTime };
142
+ }
143
+
144
+ case "memory": {
145
+ // Handle both formats: { message: "content" } or { memory: { id, type, content } }
146
+ const memoryContent = payload.message || payload.memory?.content;
147
+ if (memoryContent) {
148
+ const newMemory: MemoryItem = {
149
+ id: payload.memory?.id || `memory-${Date.now()}`,
150
+ type: payload.memory?.type || "unknown",
151
+ content: memoryContent,
152
+ timestamp: Date.now(),
153
+ };
154
+ return {
155
+ ...prev,
156
+ status: newStatus,
157
+ memory: [...prev.memory, newMemory],
158
+ firstMessageTime,
159
+ };
160
+ }
161
+ return { ...prev, status: newStatus, firstMessageTime };
162
+ }
163
+
164
+ case "response":
165
+ return {
166
+ ...prev,
167
+ status: "complete",
168
+ response: payload.message || payload.content || "",
169
+ responseCompleteTime: Date.now(),
170
+ firstMessageTime: prev.firstMessageTime ?? Date.now(),
171
+ };
172
+
173
+ default:
174
+ return { ...prev, status: newStatus, firstMessageTime };
175
+ }
176
+ });
177
+ },
178
+ [topic]
179
+ );
180
+
181
+ const reset = useCallback(() => {
182
+ setState(initialAgentResponseState);
183
+ }, []);
184
+
185
+ return { state, handleMessage, reset };
186
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * useThinkingTimer Hook
3
+ *
4
+ * Tracks elapsed time during agent thinking/processing
5
+ */
6
+
7
+ import { useState, useEffect } from "react";
8
+ import type { AgentResponseStatus } from "../types";
9
+
10
+ export interface UseThinkingTimerOptions {
11
+ startTime: number | null;
12
+ endTime: number | null;
13
+ status: AgentResponseStatus;
14
+ }
15
+
16
+ /**
17
+ * Custom hook for thinking timer
18
+ * Returns elapsed time in seconds
19
+ */
20
+ export function useThinkingTimer({
21
+ startTime,
22
+ endTime,
23
+ status,
24
+ }: UseThinkingTimerOptions): number {
25
+ const [elapsed, setElapsed] = useState(0);
26
+
27
+ useEffect(() => {
28
+ if (!startTime) {
29
+ setElapsed(0);
30
+ return;
31
+ }
32
+
33
+ // If complete, calculate final elapsed time
34
+ if (status === "complete" && endTime) {
35
+ setElapsed((endTime - startTime) / 1000);
36
+ return;
37
+ }
38
+
39
+ // If still processing, update every second
40
+ if (status === "processing") {
41
+ const updateElapsed = () => {
42
+ setElapsed((Date.now() - startTime) / 1000);
43
+ };
44
+
45
+ updateElapsed(); // Initial update
46
+ const interval = setInterval(updateElapsed, 1000);
47
+ return () => clearInterval(interval);
48
+ }
49
+ }, [startTime, endTime, status]);
50
+
51
+ return elapsed;
52
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Agent Response Component Library
3
+ *
4
+ * A library-ready component for displaying AI agent responses with
5
+ * thinking, tool calls, knowledge retrieval, memory access, and final response.
6
+ */
7
+
8
+ // Main component
9
+ export { AgentResponse } from "./AgentResponse";
10
+ export type { AgentResponseProps } from "./AgentResponse";
11
+
12
+ // Sub-components (for advanced customization)
13
+ export {
14
+ ActivityIndicators,
15
+ MetadataRow,
16
+ ThinkingSection,
17
+ ActionBar,
18
+ type ActivityIndicatorsProps,
19
+ type MetadataRowProps,
20
+ type ThinkingSectionProps,
21
+ type ActionBarProps,
22
+ } from "./components";
23
+
24
+ // Hooks
25
+ export {
26
+ useAgentResponseAccumulator,
27
+ useThinkingTimer,
28
+ type UseAgentResponseAccumulatorOptions,
29
+ type UseAgentResponseAccumulatorReturn,
30
+ type UseThinkingTimerOptions,
31
+ } from "./hooks";
32
+
33
+ // Types
34
+ export type {
35
+ AgentResponseState,
36
+ AgentResponseStatus,
37
+ FeedbackValue,
38
+ ToolCall,
39
+ KnowledgeItem,
40
+ MemoryItem,
41
+ AgentMessage,
42
+ GenericWebSocketMessage,
43
+ } from "./types";
44
+
45
+ export { initialAgentResponseState } from "./types";
46
+
47
+ // Utilities
48
+ export { formatTime, formatTotalTime } from "./utils";
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Agent Response Component Types
3
+ *
4
+ * Type definitions for the library-ready agent response component
5
+ */
6
+
7
+ /**
8
+ * Status of the agent response cycle
9
+ */
10
+ export type AgentResponseStatus = "idle" | "processing" | "complete";
11
+
12
+ /**
13
+ * Feedback value for the response
14
+ */
15
+ export type FeedbackValue = "up" | "down" | null;
16
+
17
+ /**
18
+ * Tool call information from the agent
19
+ */
20
+ export interface ToolCall {
21
+ id: string;
22
+ name: string;
23
+ arguments?: Record<string, unknown>;
24
+ timestamp: number;
25
+ }
26
+
27
+ /**
28
+ * Knowledge retrieval information
29
+ */
30
+ export interface KnowledgeItem {
31
+ id: string;
32
+ source: string;
33
+ content: string;
34
+ timestamp: number;
35
+ }
36
+
37
+ /**
38
+ * Memory access information
39
+ */
40
+ export interface MemoryItem {
41
+ id: string;
42
+ type: string;
43
+ content: string;
44
+ timestamp: number;
45
+ }
46
+
47
+ /**
48
+ * State shape for the agent response component
49
+ */
50
+ export interface AgentResponseState {
51
+ /** Current status of the response cycle */
52
+ status: AgentResponseStatus;
53
+ /** Accumulated thinking/reasoning text */
54
+ thinking: string;
55
+ /** Tool calls made during processing */
56
+ toolCalls: ToolCall[];
57
+ /** Knowledge items retrieved */
58
+ knowledge: KnowledgeItem[];
59
+ /** Memory items accessed */
60
+ memory: MemoryItem[];
61
+ /** Final response text */
62
+ response: string;
63
+ /** Timestamp when first thinking message was received (for timer) */
64
+ thinkingStartTime: number | null;
65
+ /** Timestamp when response was completed (for final timer display) */
66
+ responseCompleteTime: number | null;
67
+ /** Timestamp when first message of any type was received (for total time) */
68
+ firstMessageTime: number | null;
69
+ }
70
+
71
+ /**
72
+ * WebSocket message payload for agent responses
73
+ */
74
+ export interface AgentMessage {
75
+ type: "status" | "thinking" | "tool_call" | "knowledge" | "memory" | "response";
76
+ /** Message content - for simple string payloads */
77
+ message?: string;
78
+ /** Alternative content field */
79
+ content?: string;
80
+ /** For status messages */
81
+ status?: string;
82
+ /** For tool_call messages */
83
+ tool?: {
84
+ id: string;
85
+ name: string;
86
+ arguments?: Record<string, unknown>;
87
+ };
88
+ /** For knowledge messages */
89
+ knowledge?: {
90
+ id: string;
91
+ source: string;
92
+ content: string;
93
+ };
94
+ /** For memory messages */
95
+ memory?: {
96
+ id: string;
97
+ type: string;
98
+ content: string;
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Generic websocket message wrapper type
104
+ */
105
+ export interface GenericWebSocketMessage {
106
+ topic: string;
107
+ message: AgentMessage;
108
+ accountId?: string;
109
+ }
110
+
111
+ /**
112
+ * Initial state for the agent response component
113
+ */
114
+ export const initialAgentResponseState: AgentResponseState = {
115
+ status: "idle",
116
+ thinking: "",
117
+ toolCalls: [],
118
+ knowledge: [],
119
+ memory: [],
120
+ response: "",
121
+ thinkingStartTime: null,
122
+ responseCompleteTime: null,
123
+ firstMessageTime: null,
124
+ };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Agent Response Utility Functions
3
+ */
4
+
5
+ /**
6
+ * Format elapsed time for display
7
+ * - Under 60s: "Xs" (e.g., "23s")
8
+ * - 60+ seconds while active: "M:SS" (e.g., "1:23")
9
+ * - Complete under 60s: "Xs" (e.g., "45s")
10
+ * - Complete 60-119s: "1.X min" (e.g., "1.2 min")
11
+ * - Complete 120+ s: "X.X min" (e.g., "2.5 min")
12
+ */
13
+ export function formatTime(seconds: number, isComplete: boolean): string {
14
+ if (seconds < 1) {
15
+ return isComplete ? "<1s" : "0s";
16
+ }
17
+
18
+ if (isComplete) {
19
+ // Completed state formatting
20
+ if (seconds < 60) {
21
+ return `${Math.round(seconds)}s`;
22
+ }
23
+ const minutes = seconds / 60;
24
+ return `${minutes.toFixed(1)} min`;
25
+ }
26
+
27
+ // Active state formatting
28
+ if (seconds < 60) {
29
+ return `${Math.floor(seconds)}s`;
30
+ }
31
+ const mins = Math.floor(seconds / 60);
32
+ const secs = Math.floor(seconds % 60);
33
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
34
+ }
35
+
36
+ /**
37
+ * Format total time for action bar display
38
+ */
39
+ export function formatTotalTime(seconds: number): string {
40
+ if (seconds < 1) {
41
+ return "<1s";
42
+ }
43
+ if (seconds < 60) {
44
+ return `${seconds.toFixed(1)}s`;
45
+ }
46
+ const minutes = seconds / 60;
47
+ return `${minutes.toFixed(1)}m`;
48
+ }