fixdog 0.0.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,406 @@
1
+ import { useState, useEffect, useRef } from "react";
2
+ import {
3
+ sendChatPrompt,
4
+ ChatRequest,
5
+ ChatResponse,
6
+ sendToDeveloper,
7
+ SendToDevResponse,
8
+ } from "../api/client";
9
+ import { ElementInfo } from "../types/sidebar";
10
+
11
+ interface Message {
12
+ id: string;
13
+ role: "user" | "assistant";
14
+ content: string;
15
+ timestamp: number;
16
+ isLoading?: boolean;
17
+ error?: string;
18
+ }
19
+
20
+ interface ConversationalInputProps {
21
+ elementInfo: ElementInfo;
22
+ editorUrl: string;
23
+ apiEndpoint: string;
24
+ }
25
+
26
+ export function ConversationalInputReact(props: ConversationalInputProps) {
27
+ const [userInput, setUserInput] = useState("");
28
+ const [messages, setMessages] = useState<Message[]>([]);
29
+ const [isLoading, setIsLoading] = useState(false);
30
+ const [sessionId, setSessionId] = useState<string | undefined>(undefined);
31
+ const [isCreatingPR, setIsCreatingPR] = useState(false);
32
+ const [prUrl, setPrUrl] = useState<string | null>(null);
33
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
34
+ const messagesEndRef = useRef<HTMLDivElement>(null);
35
+
36
+ useEffect(() => {
37
+ textareaRef.current?.focus();
38
+ }, []);
39
+
40
+ useEffect(() => {
41
+ // Auto-resize textarea
42
+ const textarea = textareaRef.current;
43
+ if (textarea) {
44
+ textarea.style.height = "auto";
45
+ textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
46
+ }
47
+ }, [userInput]);
48
+
49
+ useEffect(() => {
50
+ // Auto-scroll to bottom when new messages are added
51
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
52
+ }, [messages]);
53
+
54
+ const handleSubmit = async () => {
55
+ const input = userInput.trim();
56
+ if (!input || isLoading) return;
57
+
58
+ // Add user message to history
59
+ const userMessage: Message = {
60
+ id: `user-${Date.now()}`,
61
+ role: "user",
62
+ content: input,
63
+ timestamp: Date.now(),
64
+ };
65
+
66
+ setMessages((prev) => [...prev, userMessage]);
67
+ setUserInput("");
68
+ setIsLoading(true);
69
+
70
+ // Add loading message
71
+ const loadingMessageId = `loading-${Date.now()}`;
72
+ const loadingMessage: Message = {
73
+ id: loadingMessageId,
74
+ role: "assistant",
75
+ content: "",
76
+ timestamp: Date.now(),
77
+ isLoading: true,
78
+ };
79
+ setMessages((prev) => [...prev, loadingMessage]);
80
+
81
+ const request: ChatRequest = {
82
+ prompt: `The component selected by the user: ${props.editorUrl}
83
+ User request: ${input}
84
+ Update multiple files (if necessary) to achieve the user's request.`,
85
+ sessionId: sessionId,
86
+ };
87
+
88
+ try {
89
+ const result: ChatResponse = await sendChatPrompt(
90
+ request,
91
+ props.apiEndpoint
92
+ );
93
+
94
+ // Save sessionId from response if provided
95
+ if (result.sessionId && !sessionId) {
96
+ setSessionId(result.sessionId);
97
+ }
98
+
99
+ // Remove loading message and add response
100
+ setMessages((prev) => {
101
+ const filtered = prev.filter((msg) => msg.id !== loadingMessageId);
102
+ const assistantMessage: Message = {
103
+ id: `assistant-${Date.now()}`,
104
+ role: "assistant",
105
+ content: result.ok
106
+ ? result.message
107
+ : result.error || "Request failed",
108
+ timestamp: Date.now(),
109
+ error: !result.ok ? result.error : undefined,
110
+ };
111
+ return [...filtered, assistantMessage];
112
+ });
113
+ } catch (err) {
114
+ // Remove loading message and add error
115
+ setMessages((prev) => {
116
+ const filtered = prev.filter((msg) => msg.id !== loadingMessageId);
117
+ const errorMessage: Message = {
118
+ id: `error-${Date.now()}`,
119
+ role: "assistant",
120
+ content:
121
+ err instanceof Error ? err.message : "Unknown error occurred",
122
+ timestamp: Date.now(),
123
+ error: err instanceof Error ? err.message : "Unknown error occurred",
124
+ };
125
+ return [...filtered, errorMessage];
126
+ });
127
+ } finally {
128
+ setIsLoading(false);
129
+ }
130
+ };
131
+
132
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
133
+ // Submit on Cmd/Ctrl + Enter
134
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
135
+ e.preventDefault();
136
+ handleSubmit();
137
+ }
138
+ };
139
+
140
+ const handleSendToDeveloper = async () => {
141
+ if (!sessionId || isCreatingPR) return;
142
+
143
+ setIsCreatingPR(true);
144
+ setPrUrl(null);
145
+
146
+ try {
147
+ const result: SendToDevResponse = await sendToDeveloper(
148
+ { sessionId },
149
+ props.apiEndpoint
150
+ );
151
+
152
+ if (result.ok && result.prUrl) {
153
+ setPrUrl(result.prUrl);
154
+ // Add success message
155
+ const successMessage: Message = {
156
+ id: `pr-success-${Date.now()}`,
157
+ role: "assistant",
158
+ content: `Pull request created successfully!`,
159
+ timestamp: Date.now(),
160
+ };
161
+ setMessages((prev) => [...prev, successMessage]);
162
+ } else {
163
+ // Add error message
164
+ const errorMessage: Message = {
165
+ id: `pr-error-${Date.now()}`,
166
+ role: "assistant",
167
+ content: result.error || "Failed to create pull request",
168
+ timestamp: Date.now(),
169
+ error: result.error || "Failed to create pull request",
170
+ };
171
+ setMessages((prev) => [...prev, errorMessage]);
172
+ }
173
+ } catch (err) {
174
+ const errorMessage: Message = {
175
+ id: `pr-error-${Date.now()}`,
176
+ role: "assistant",
177
+ content: err instanceof Error ? err.message : "Unknown error occurred",
178
+ timestamp: Date.now(),
179
+ error: err instanceof Error ? err.message : "Unknown error occurred",
180
+ };
181
+ setMessages((prev) => [...prev, errorMessage]);
182
+ } finally {
183
+ setIsCreatingPR(false);
184
+ }
185
+ };
186
+
187
+ const handleRetry = async (messageId: string) => {
188
+ // Find the user message that corresponds to this error
189
+ const errorIndex = messages.findIndex((msg) => msg.id === messageId);
190
+ if (errorIndex === -1) return;
191
+
192
+ // Find the previous user message
193
+ let userMessageIndex = -1;
194
+ for (let i = errorIndex - 1; i >= 0; i--) {
195
+ if (messages[i].role === "user") {
196
+ userMessageIndex = i;
197
+ break;
198
+ }
199
+ }
200
+
201
+ if (userMessageIndex === -1) return;
202
+
203
+ const userMessage = messages[userMessageIndex];
204
+ const input = userMessage.content;
205
+
206
+ // Remove error message
207
+ setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
208
+
209
+ setIsLoading(true);
210
+
211
+ // Add loading message
212
+ const loadingMessageId = `loading-${Date.now()}`;
213
+ const loadingMessage: Message = {
214
+ id: loadingMessageId,
215
+ role: "assistant",
216
+ content: "",
217
+ timestamp: Date.now(),
218
+ isLoading: true,
219
+ };
220
+ setMessages((prev) => [...prev, loadingMessage]);
221
+
222
+ const request: ChatRequest = {
223
+ prompt: `The component selected by the user: ${props.editorUrl}
224
+ User request: ${input}
225
+ Update multiple files (if necessary) to achieve the user's request.`,
226
+ sessionId: sessionId,
227
+ };
228
+
229
+ try {
230
+ const result: ChatResponse = await sendChatPrompt(
231
+ request,
232
+ props.apiEndpoint
233
+ );
234
+
235
+ // Save sessionId from response if provided
236
+ if (result.sessionId && !sessionId) {
237
+ setSessionId(result.sessionId);
238
+ }
239
+
240
+ // Remove loading message and add response
241
+ setMessages((prev) => {
242
+ const filtered = prev.filter((msg) => msg.id !== loadingMessageId);
243
+ const assistantMessage: Message = {
244
+ id: `assistant-${Date.now()}`,
245
+ role: "assistant",
246
+ content: result.ok
247
+ ? result.message
248
+ : result.error || "Request failed",
249
+ timestamp: Date.now(),
250
+ error: !result.ok ? result.error : undefined,
251
+ };
252
+ return [...filtered, assistantMessage];
253
+ });
254
+ } catch (err) {
255
+ // Remove loading message and add error
256
+ setMessages((prev) => {
257
+ const filtered = prev.filter((msg) => msg.id !== loadingMessageId);
258
+ const errorMessage: Message = {
259
+ id: `error-${Date.now()}`,
260
+ role: "assistant",
261
+ content:
262
+ err instanceof Error ? err.message : "Unknown error occurred",
263
+ timestamp: Date.now(),
264
+ error: err instanceof Error ? err.message : "Unknown error occurred",
265
+ };
266
+ return [...filtered, errorMessage];
267
+ });
268
+ } finally {
269
+ setIsLoading(false);
270
+ }
271
+ };
272
+
273
+ return (
274
+ <>
275
+ {/* Messages Area */}
276
+ <div className="uidog-messages-container">
277
+ {messages.length === 0 && (
278
+ <div className="uidog-empty-state">
279
+ <div className="uidog-empty-state-text">
280
+ What changes would you like to make?
281
+ </div>
282
+ </div>
283
+ )}
284
+ {messages.map((message) => (
285
+ <div
286
+ key={message.id}
287
+ className={`uidog-message uidog-message-${message.role}`}
288
+ >
289
+ <div className="uidog-message-bubble">
290
+ {message.isLoading ? (
291
+ <div className="uidog-message-loading">
292
+ <div className="uidog-spinner"></div>
293
+ <span>Processing your request...</span>
294
+ </div>
295
+ ) : message.error ? (
296
+ <>
297
+ <div className="uidog-message-content uidog-message-error">
298
+ {message.content}
299
+ </div>
300
+ <button
301
+ className="uidog-retry-btn"
302
+ onClick={() => handleRetry(message.id)}
303
+ >
304
+ Retry
305
+ </button>
306
+ </>
307
+ ) : (
308
+ <div className="uidog-message-content">{message.content}</div>
309
+ )}
310
+ </div>
311
+ </div>
312
+ ))}
313
+ <div ref={messagesEndRef} />
314
+ </div>
315
+
316
+ {/* Fixed Input Footer */}
317
+ <div className="uidog-input-footer-fixed">
318
+ <div className="uidog-input-wrapper">
319
+ <textarea
320
+ id="uidog-textarea"
321
+ ref={textareaRef}
322
+ className="uidog-textarea"
323
+ placeholder="Describe the changes you want..."
324
+ value={userInput}
325
+ onChange={(e) => setUserInput(e.target.value)}
326
+ onKeyDown={handleKeyDown}
327
+ disabled={isLoading}
328
+ />
329
+ <button
330
+ className="uidog-submit-btn"
331
+ onClick={handleSubmit}
332
+ disabled={!userInput.trim() || isLoading}
333
+ title="Send message (Cmd/Ctrl + Enter)"
334
+ >
335
+ {isLoading ? (
336
+ <div className="uidog-spinner-small"></div>
337
+ ) : (
338
+ <svg
339
+ width="16"
340
+ height="16"
341
+ viewBox="0 0 16 16"
342
+ fill="none"
343
+ xmlns="http://www.w3.org/2000/svg"
344
+ >
345
+ <path
346
+ d="M8 2L8 14M8 2L2 8M8 2L14 8"
347
+ stroke="currentColor"
348
+ strokeWidth="2"
349
+ strokeLinecap="round"
350
+ strokeLinejoin="round"
351
+ />
352
+ </svg>
353
+ )}
354
+ </button>
355
+ </div>
356
+ <div className="uidog-input-hint">Cmd/Ctrl + Enter to submit</div>
357
+ {sessionId && (
358
+ <div className="uidog-send-to-dev-section">
359
+ <button
360
+ className="uidog-send-to-dev-btn"
361
+ onClick={handleSendToDeveloper}
362
+ disabled={isCreatingPR || isLoading}
363
+ title="Create PR with changes"
364
+ >
365
+ {isCreatingPR ? (
366
+ <>
367
+ <div className="uidog-spinner-small"></div>
368
+ <span>Creating PR...</span>
369
+ </>
370
+ ) : (
371
+ <>
372
+ <svg
373
+ width="16"
374
+ height="16"
375
+ viewBox="0 0 16 16"
376
+ fill="none"
377
+ xmlns="http://www.w3.org/2000/svg"
378
+ >
379
+ <path
380
+ d="M8 1L8 15M8 1L1 8M8 1L15 8"
381
+ stroke="currentColor"
382
+ strokeWidth="2"
383
+ strokeLinecap="round"
384
+ strokeLinejoin="round"
385
+ />
386
+ </svg>
387
+ <span>Submit to Developer</span>
388
+ </>
389
+ )}
390
+ </button>
391
+ {prUrl && (
392
+ <a
393
+ href={prUrl}
394
+ target="_blank"
395
+ rel="noopener noreferrer"
396
+ className="uidog-pr-link"
397
+ >
398
+ View PR →
399
+ </a>
400
+ )}
401
+ </div>
402
+ )}
403
+ </div>
404
+ </>
405
+ );
406
+ }
@@ -0,0 +1,84 @@
1
+ import type { ElementInfo } from "../types";
2
+
3
+ interface ElementInfoDisplayProps {
4
+ elementInfo: ElementInfo;
5
+ onClose: () => void;
6
+ }
7
+
8
+ export function ElementInfoDisplayReact(props: ElementInfoDisplayProps) {
9
+ const isDomSnapshot = props.elementInfo.kind === "dom";
10
+
11
+ if (isDomSnapshot) {
12
+ const dom = props.elementInfo.domSnapshot;
13
+ const outerHTML = dom?.outerHTML || "No HTML available";
14
+ const text = dom?.text || "";
15
+ const attributes = dom?.attributes || {};
16
+
17
+ return (
18
+ <div className="uidog-element-info">
19
+ <div className="uidog-element-info-content">
20
+ <div className="uidog-file-location">
21
+ Server-rendered DOM (no source available)
22
+ </div>
23
+ <button
24
+ className="uidog-close-btn"
25
+ onClick={props.onClose}
26
+ title="Close sidebar (ESC)"
27
+ aria-label="Close sidebar"
28
+ >
29
+ ×
30
+ </button>
31
+ </div>
32
+
33
+ <div className="uidog-dom-snapshot">
34
+ <div className="uidog-dom-label">outerHTML (trimmed):</div>
35
+ <pre className="uidog-dom-snippet">{outerHTML}</pre>
36
+
37
+ {text && (
38
+ <>
39
+ <div className="uidog-dom-label">textContent (trimmed):</div>
40
+ <pre className="uidog-dom-snippet">{text}</pre>
41
+ </>
42
+ )}
43
+
44
+ {Object.keys(attributes).length > 0 && (
45
+ <div className="uidog-dom-attributes">
46
+ <div className="uidog-dom-label">attributes:</div>
47
+ <ul>
48
+ {Object.entries(attributes).map(([key, value]) => (
49
+ <li key={key}>
50
+ <code>{key}</code>=<code>{value}</code>
51
+ </li>
52
+ ))}
53
+ </ul>
54
+ </div>
55
+ )}
56
+
57
+ <div className="uidog-dom-hint">
58
+ To see file/line info, render this DOM through a small client
59
+ boundary.
60
+ </div>
61
+ </div>
62
+ </div>
63
+ );
64
+ }
65
+
66
+ const fileName = props.elementInfo.filePath?.split("/").pop() || "";
67
+ const fileLocation = `Selected element at ${fileName}`;
68
+
69
+ return (
70
+ <div className="uidog-element-info">
71
+ <div className="uidog-element-info-content">
72
+ <span className="uidog-file-location">{fileLocation}</span>
73
+ <button
74
+ className="uidog-close-btn"
75
+ onClick={props.onClose}
76
+ title="Close sidebar (ESC)"
77
+ aria-label="Close sidebar"
78
+ >
79
+ ×
80
+ </button>
81
+ </div>
82
+ </div>
83
+ );
84
+ }
@@ -0,0 +1,49 @@
1
+ import { useEffect } from "react";
2
+ import type { ElementInfo } from "../types";
3
+ import { ElementInfoDisplayReact } from "./ElementInfoDisplayReact";
4
+ import { ConversationalInputReact } from "./ConversationalInputReact";
5
+
6
+ interface UiDogSidebarProps {
7
+ elementInfo: ElementInfo;
8
+ editorUrl: string;
9
+ onClose: () => void;
10
+ apiEndpoint: string;
11
+ }
12
+
13
+ export function UiDogSidebarReact(props: UiDogSidebarProps) {
14
+ useEffect(() => {
15
+ const handleEscapeKey = (e: KeyboardEvent) => {
16
+ if (e.key === "Escape") {
17
+ props.onClose();
18
+ }
19
+ };
20
+
21
+ document.addEventListener("keydown", handleEscapeKey);
22
+ return () => {
23
+ document.removeEventListener("keydown", handleEscapeKey);
24
+ };
25
+ }, [props]);
26
+
27
+ return (
28
+ <div className="uidog-sidebar-overlay">
29
+ <div className="uidog-sidebar">
30
+ {/* Element Info Section */}
31
+ <div className="uidog-element-info-section">
32
+ <ElementInfoDisplayReact
33
+ elementInfo={props.elementInfo}
34
+ onClose={props.onClose}
35
+ />
36
+ </div>
37
+
38
+ {/* Chat Area - Messages + Input */}
39
+ <div className="uidog-chat-container">
40
+ <ConversationalInputReact
41
+ elementInfo={props.elementInfo}
42
+ editorUrl={props.editorUrl}
43
+ apiEndpoint={props.apiEndpoint}
44
+ />
45
+ </div>
46
+ </div>
47
+ </div>
48
+ );
49
+ }