@usesidekick/react 0.1.0
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/README.md +246 -0
- package/dist/index.d.mts +358 -0
- package/dist/index.d.ts +358 -0
- package/dist/index.js +2470 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2403 -0
- package/dist/index.mjs.map +1 -0
- package/dist/jsx-dev-runtime.d.mts +21 -0
- package/dist/jsx-dev-runtime.d.ts +21 -0
- package/dist/jsx-dev-runtime.js +160 -0
- package/dist/jsx-dev-runtime.js.map +1 -0
- package/dist/jsx-dev-runtime.mjs +122 -0
- package/dist/jsx-dev-runtime.mjs.map +1 -0
- package/dist/jsx-runtime.d.mts +26 -0
- package/dist/jsx-runtime.d.ts +26 -0
- package/dist/jsx-runtime.js +150 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/dist/jsx-runtime.mjs +109 -0
- package/dist/jsx-runtime.mjs.map +1 -0
- package/dist/server/index.d.mts +235 -0
- package/dist/server/index.d.ts +235 -0
- package/dist/server/index.js +642 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/index.mjs +597 -0
- package/dist/server/index.mjs.map +1 -0
- package/package.json +64 -0
- package/src/components/SidekickPanel.tsx +868 -0
- package/src/components/index.ts +1 -0
- package/src/context.tsx +157 -0
- package/src/flags.ts +47 -0
- package/src/index.ts +71 -0
- package/src/jsx-dev-runtime.ts +138 -0
- package/src/jsx-runtime.ts +159 -0
- package/src/loader.ts +35 -0
- package/src/primitives/behavior.ts +70 -0
- package/src/primitives/data.ts +91 -0
- package/src/primitives/index.ts +3 -0
- package/src/primitives/ui.ts +268 -0
- package/src/provider.tsx +1264 -0
- package/src/runtime-loader.ts +106 -0
- package/src/server/drizzle-adapter.ts +53 -0
- package/src/server/drizzle-schema.ts +16 -0
- package/src/server/generate.ts +578 -0
- package/src/server/handler.ts +343 -0
- package/src/server/index.ts +20 -0
- package/src/server/storage.ts +1 -0
- package/src/server/types.ts +49 -0
- package/src/types.ts +295 -0
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useEffect, FormEvent } from 'react';
|
|
4
|
+
import { useSidekickSafe } from '../context';
|
|
5
|
+
import { setFlag } from '../flags';
|
|
6
|
+
|
|
7
|
+
interface Message {
|
|
8
|
+
id: string;
|
|
9
|
+
role: 'user' | 'assistant' | 'system';
|
|
10
|
+
content: string;
|
|
11
|
+
timestamp: Date;
|
|
12
|
+
status?: 'pending' | 'generating' | 'success' | 'error';
|
|
13
|
+
overrideId?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface SidekickPanelProps {
|
|
17
|
+
apiEndpoint?: string;
|
|
18
|
+
deleteEndpoint?: string;
|
|
19
|
+
toggleEndpoint?: string;
|
|
20
|
+
onGenerate?: (request: string) => Promise<{ success: boolean; overrideId?: string; error?: string }>;
|
|
21
|
+
onDelete?: (overrideId: string) => Promise<{ success: boolean; error?: string }>;
|
|
22
|
+
onToggle?: (overrideId: string, enabled: boolean) => Promise<{ success: boolean; error?: string }>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function SidekickPanel({ apiEndpoint, deleteEndpoint, toggleEndpoint, onGenerate, onDelete, onToggle }: SidekickPanelProps) {
|
|
26
|
+
// Initialize state from sessionStorage to persist across reloads
|
|
27
|
+
const [isOpen, setIsOpen] = useState(() => {
|
|
28
|
+
if (typeof window === 'undefined') return false;
|
|
29
|
+
return sessionStorage.getItem('sidekick_panel_open') === 'true';
|
|
30
|
+
});
|
|
31
|
+
const [activeTab, setActiveTab] = useState<'chat' | 'overrides'>(() => {
|
|
32
|
+
if (typeof window === 'undefined') return 'chat';
|
|
33
|
+
return (sessionStorage.getItem('sidekick_panel_tab') as 'chat' | 'overrides') || 'chat';
|
|
34
|
+
});
|
|
35
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
36
|
+
const [input, setInput] = useState('');
|
|
37
|
+
const [isGenerating, setIsGenerating] = useState(false);
|
|
38
|
+
const [deletingId, setDeletingId] = useState<string | null>(null);
|
|
39
|
+
const [togglingId, setTogglingId] = useState<string | null>(null);
|
|
40
|
+
const [editingOverrideId, setEditingOverrideId] = useState<string | null>(null);
|
|
41
|
+
|
|
42
|
+
const context = useSidekickSafe();
|
|
43
|
+
const overrides = context?.overrides ?? [];
|
|
44
|
+
|
|
45
|
+
// Compute endpoints from apiEndpoint if not provided
|
|
46
|
+
const effectiveDeleteEndpoint = deleteEndpoint || (apiEndpoint ? apiEndpoint.replace('/generate', '/delete') : undefined);
|
|
47
|
+
const effectiveToggleEndpoint = toggleEndpoint || (apiEndpoint ? apiEndpoint.replace('/generate', '/toggle') : undefined);
|
|
48
|
+
|
|
49
|
+
// Persist panel state changes to sessionStorage
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (typeof window === 'undefined') return;
|
|
52
|
+
sessionStorage.setItem('sidekick_panel_open', isOpen ? 'true' : 'false');
|
|
53
|
+
}, [isOpen]);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (typeof window === 'undefined') return;
|
|
57
|
+
sessionStorage.setItem('sidekick_panel_tab', activeTab);
|
|
58
|
+
}, [activeTab]);
|
|
59
|
+
|
|
60
|
+
// Compute sessionStorage key based on editing mode
|
|
61
|
+
const messagesStorageKey = editingOverrideId
|
|
62
|
+
? `sidekick_edit_messages_${editingOverrideId}`
|
|
63
|
+
: 'sidekick_messages';
|
|
64
|
+
|
|
65
|
+
// Load message history from sessionStorage (re-runs when editing mode changes)
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (typeof window === 'undefined') return;
|
|
68
|
+
const stored = sessionStorage.getItem(messagesStorageKey);
|
|
69
|
+
if (stored) {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(stored);
|
|
72
|
+
setMessages(parsed.map((m: Message) => ({ ...m, timestamp: new Date(m.timestamp) })));
|
|
73
|
+
} catch {
|
|
74
|
+
setMessages([]);
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
setMessages([]);
|
|
78
|
+
}
|
|
79
|
+
}, [messagesStorageKey]);
|
|
80
|
+
|
|
81
|
+
// Save messages to sessionStorage
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (typeof window === 'undefined' || messages.length === 0) return;
|
|
84
|
+
sessionStorage.setItem(messagesStorageKey, JSON.stringify(messages));
|
|
85
|
+
}, [messages, messagesStorageKey]);
|
|
86
|
+
|
|
87
|
+
const handleSubmit = useCallback(async (e: FormEvent) => {
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
if (!input.trim() || isGenerating) return;
|
|
90
|
+
|
|
91
|
+
const userMessage: Message = {
|
|
92
|
+
id: generateId(),
|
|
93
|
+
role: 'user',
|
|
94
|
+
content: input.trim(),
|
|
95
|
+
timestamp: new Date(),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const assistantMessage: Message = {
|
|
99
|
+
id: generateId(),
|
|
100
|
+
role: 'assistant',
|
|
101
|
+
content: 'Generating your customization...',
|
|
102
|
+
timestamp: new Date(),
|
|
103
|
+
status: 'generating',
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
setMessages((prev) => [...prev, userMessage, assistantMessage]);
|
|
107
|
+
setInput('');
|
|
108
|
+
setIsGenerating(true);
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
let result: { success: boolean; overrideId?: string; name?: string; description?: string; error?: string };
|
|
112
|
+
|
|
113
|
+
const requestBody: { request: string; overrideId?: string } = { request: userMessage.content };
|
|
114
|
+
if (editingOverrideId) {
|
|
115
|
+
requestBody.overrideId = editingOverrideId;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (onGenerate) {
|
|
119
|
+
result = await onGenerate(userMessage.content);
|
|
120
|
+
} else if (apiEndpoint) {
|
|
121
|
+
const response = await fetch(apiEndpoint, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: { 'Content-Type': 'application/json' },
|
|
124
|
+
body: JSON.stringify(requestBody),
|
|
125
|
+
});
|
|
126
|
+
result = await response.json();
|
|
127
|
+
} else {
|
|
128
|
+
result = {
|
|
129
|
+
success: false,
|
|
130
|
+
error: 'No API endpoint or onGenerate handler configured',
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const verb = editingOverrideId ? 'Updated' : 'Created';
|
|
135
|
+
const successMessage = result.success
|
|
136
|
+
? `${verb} "${result.name || result.overrideId}"!\n\n${result.description || ''}\n\nReload the page to apply changes.`
|
|
137
|
+
: `Failed to generate: ${result.error}`;
|
|
138
|
+
|
|
139
|
+
setMessages((prev) =>
|
|
140
|
+
prev.map((m) =>
|
|
141
|
+
m.id === assistantMessage.id
|
|
142
|
+
? {
|
|
143
|
+
...m,
|
|
144
|
+
content: successMessage,
|
|
145
|
+
status: result.success ? 'success' : 'error',
|
|
146
|
+
overrideId: result.overrideId,
|
|
147
|
+
}
|
|
148
|
+
: m
|
|
149
|
+
)
|
|
150
|
+
);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
setMessages((prev) =>
|
|
153
|
+
prev.map((m) =>
|
|
154
|
+
m.id === assistantMessage.id
|
|
155
|
+
? {
|
|
156
|
+
...m,
|
|
157
|
+
content: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
158
|
+
status: 'error',
|
|
159
|
+
}
|
|
160
|
+
: m
|
|
161
|
+
)
|
|
162
|
+
);
|
|
163
|
+
} finally {
|
|
164
|
+
setIsGenerating(false);
|
|
165
|
+
}
|
|
166
|
+
}, [input, isGenerating, onGenerate, apiEndpoint, editingOverrideId]);
|
|
167
|
+
|
|
168
|
+
const handleToggleOverride = useCallback(async (overrideId: string, enabled: boolean) => {
|
|
169
|
+
setTogglingId(overrideId);
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
if (onToggle) {
|
|
173
|
+
await onToggle(overrideId, enabled);
|
|
174
|
+
} else if (effectiveToggleEndpoint) {
|
|
175
|
+
const response = await fetch(effectiveToggleEndpoint, {
|
|
176
|
+
method: 'POST',
|
|
177
|
+
headers: { 'Content-Type': 'application/json' },
|
|
178
|
+
body: JSON.stringify({ overrideId, enabled }),
|
|
179
|
+
});
|
|
180
|
+
const result = await response.json();
|
|
181
|
+
if (!result.success) {
|
|
182
|
+
throw new Error(result.error || 'Toggle failed');
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
// Fallback to localStorage
|
|
186
|
+
setFlag(overrideId, enabled);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Reload the page to apply the change
|
|
190
|
+
window.location.reload();
|
|
191
|
+
} catch (error) {
|
|
192
|
+
alert(`Failed to toggle: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
193
|
+
setTogglingId(null);
|
|
194
|
+
}
|
|
195
|
+
}, [onToggle, effectiveToggleEndpoint]);
|
|
196
|
+
|
|
197
|
+
const handleDeleteOverride = useCallback(async (overrideId: string) => {
|
|
198
|
+
if (!confirm(`Are you sure you want to delete this override? This cannot be undone.`)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
setDeletingId(overrideId);
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
let result: { success: boolean; error?: string };
|
|
206
|
+
|
|
207
|
+
if (onDelete) {
|
|
208
|
+
result = await onDelete(overrideId);
|
|
209
|
+
} else if (effectiveDeleteEndpoint) {
|
|
210
|
+
const response = await fetch(effectiveDeleteEndpoint, {
|
|
211
|
+
method: 'POST',
|
|
212
|
+
headers: { 'Content-Type': 'application/json' },
|
|
213
|
+
body: JSON.stringify({ overrideId }),
|
|
214
|
+
});
|
|
215
|
+
result = await response.json();
|
|
216
|
+
} else {
|
|
217
|
+
result = { success: false, error: 'No delete endpoint configured' };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (result.success) {
|
|
221
|
+
// Clear the flag and reload
|
|
222
|
+
setFlag(overrideId, false);
|
|
223
|
+
window.location.reload();
|
|
224
|
+
} else {
|
|
225
|
+
alert(`Failed to delete: ${result.error}`);
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
alert(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
229
|
+
} finally {
|
|
230
|
+
setDeletingId(null);
|
|
231
|
+
}
|
|
232
|
+
}, [onDelete, effectiveDeleteEndpoint]);
|
|
233
|
+
|
|
234
|
+
const handleRegenerateOverride = useCallback((overrideId: string) => {
|
|
235
|
+
setEditingOverrideId(overrideId);
|
|
236
|
+
setActiveTab('chat');
|
|
237
|
+
setInput('');
|
|
238
|
+
}, []);
|
|
239
|
+
|
|
240
|
+
const handleEditOverride = useCallback((overrideId: string) => {
|
|
241
|
+
setEditingOverrideId(overrideId);
|
|
242
|
+
setActiveTab('chat');
|
|
243
|
+
setInput('');
|
|
244
|
+
}, []);
|
|
245
|
+
|
|
246
|
+
const handleExitEditMode = useCallback(() => {
|
|
247
|
+
setEditingOverrideId(null);
|
|
248
|
+
setInput('');
|
|
249
|
+
}, []);
|
|
250
|
+
|
|
251
|
+
const handleClearHistory = useCallback(() => {
|
|
252
|
+
setMessages([]);
|
|
253
|
+
sessionStorage.removeItem(messagesStorageKey);
|
|
254
|
+
}, [messagesStorageKey]);
|
|
255
|
+
|
|
256
|
+
if (!isOpen) {
|
|
257
|
+
return (
|
|
258
|
+
<button
|
|
259
|
+
onClick={() => setIsOpen(true)}
|
|
260
|
+
style={styles.trigger}
|
|
261
|
+
aria-label="Open Sidekick Panel"
|
|
262
|
+
>
|
|
263
|
+
<SidekickIcon />
|
|
264
|
+
</button>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return (
|
|
269
|
+
<div style={styles.overlay} onWheel={(e) => e.stopPropagation()} onTouchMove={(e) => e.stopPropagation()}>
|
|
270
|
+
<div style={styles.panel}>
|
|
271
|
+
{/* Header */}
|
|
272
|
+
<div style={styles.header}>
|
|
273
|
+
<h2 style={styles.title}>Sidekick</h2>
|
|
274
|
+
<button onClick={() => setIsOpen(false)} style={styles.closeButton}>
|
|
275
|
+
<CloseIcon />
|
|
276
|
+
</button>
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
{/* Tabs */}
|
|
280
|
+
<div style={styles.tabs}>
|
|
281
|
+
<button
|
|
282
|
+
onClick={() => setActiveTab('chat')}
|
|
283
|
+
style={{
|
|
284
|
+
...styles.tab,
|
|
285
|
+
...(activeTab === 'chat' ? styles.tabActive : {}),
|
|
286
|
+
}}
|
|
287
|
+
>
|
|
288
|
+
Customize
|
|
289
|
+
</button>
|
|
290
|
+
<button
|
|
291
|
+
onClick={() => setActiveTab('overrides')}
|
|
292
|
+
style={{
|
|
293
|
+
...styles.tab,
|
|
294
|
+
...(activeTab === 'overrides' ? styles.tabActive : {}),
|
|
295
|
+
}}
|
|
296
|
+
>
|
|
297
|
+
Overrides ({overrides.length})
|
|
298
|
+
</button>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
{/* Content */}
|
|
302
|
+
<div style={styles.content}>
|
|
303
|
+
{activeTab === 'chat' ? (
|
|
304
|
+
<ChatTab
|
|
305
|
+
messages={messages}
|
|
306
|
+
input={input}
|
|
307
|
+
isGenerating={isGenerating}
|
|
308
|
+
onInputChange={setInput}
|
|
309
|
+
onSubmit={handleSubmit}
|
|
310
|
+
onClearHistory={handleClearHistory}
|
|
311
|
+
editingOverride={editingOverrideId ? overrides.find(o => o.id === editingOverrideId) : undefined}
|
|
312
|
+
onExitEditMode={handleExitEditMode}
|
|
313
|
+
/>
|
|
314
|
+
) : (
|
|
315
|
+
<OverridesTab
|
|
316
|
+
overrides={overrides}
|
|
317
|
+
onToggle={handleToggleOverride}
|
|
318
|
+
onDelete={handleDeleteOverride}
|
|
319
|
+
onRegenerate={handleRegenerateOverride}
|
|
320
|
+
onEdit={handleEditOverride}
|
|
321
|
+
deletingId={deletingId}
|
|
322
|
+
togglingId={togglingId}
|
|
323
|
+
/>
|
|
324
|
+
)}
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
interface ChatTabProps {
|
|
332
|
+
messages: Message[];
|
|
333
|
+
input: string;
|
|
334
|
+
isGenerating: boolean;
|
|
335
|
+
onInputChange: (value: string) => void;
|
|
336
|
+
onSubmit: (e: FormEvent) => void;
|
|
337
|
+
onClearHistory: () => void;
|
|
338
|
+
editingOverride?: { id: string; name: string; description: string; enabled: boolean };
|
|
339
|
+
onExitEditMode: () => void;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function ChatTab({
|
|
343
|
+
messages,
|
|
344
|
+
input,
|
|
345
|
+
isGenerating,
|
|
346
|
+
onInputChange,
|
|
347
|
+
onSubmit,
|
|
348
|
+
onClearHistory,
|
|
349
|
+
editingOverride,
|
|
350
|
+
onExitEditMode,
|
|
351
|
+
}: ChatTabProps) {
|
|
352
|
+
return (
|
|
353
|
+
<div style={styles.chatContainer}>
|
|
354
|
+
{/* Edit mode header */}
|
|
355
|
+
{editingOverride && (
|
|
356
|
+
<div style={styles.editHeader}>
|
|
357
|
+
<span style={styles.editHeaderText}>Editing: {editingOverride.name}</span>
|
|
358
|
+
<button onClick={onExitEditMode} style={styles.editHeaderClose}>✕</button>
|
|
359
|
+
</div>
|
|
360
|
+
)}
|
|
361
|
+
{/* Messages */}
|
|
362
|
+
<div style={styles.messages}>
|
|
363
|
+
{messages.length === 0 ? (
|
|
364
|
+
editingOverride ? (
|
|
365
|
+
<div style={styles.emptyState}>
|
|
366
|
+
<p style={styles.emptyTitle}>Edit "{editingOverride.name}"</p>
|
|
367
|
+
<p style={styles.emptyDescription}>
|
|
368
|
+
Describe what you want to change about this override, and I'll modify the existing code.
|
|
369
|
+
</p>
|
|
370
|
+
<div style={styles.examples}>
|
|
371
|
+
<p style={styles.examplesTitle}>Try saying:</p>
|
|
372
|
+
<ul style={styles.examplesList}>
|
|
373
|
+
<li>"Change the color to red"</li>
|
|
374
|
+
<li>"Make it only apply to high priority items"</li>
|
|
375
|
+
<li>"Add a hover effect"</li>
|
|
376
|
+
</ul>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
) : (
|
|
380
|
+
<div style={styles.emptyState}>
|
|
381
|
+
<p style={styles.emptyTitle}>Customize your app</p>
|
|
382
|
+
<p style={styles.emptyDescription}>
|
|
383
|
+
Describe what you want to change in natural language, and I'll generate the code.
|
|
384
|
+
</p>
|
|
385
|
+
<div style={styles.examples}>
|
|
386
|
+
<p style={styles.examplesTitle}>Try saying:</p>
|
|
387
|
+
<ul style={styles.examplesList}>
|
|
388
|
+
<li>"Add a column showing days until due date"</li>
|
|
389
|
+
<li>"Add a banner at the top of the sidebar"</li>
|
|
390
|
+
<li>"Highlight overdue tasks in red"</li>
|
|
391
|
+
</ul>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
)
|
|
395
|
+
) : (
|
|
396
|
+
<>
|
|
397
|
+
{messages.map((message) => (
|
|
398
|
+
<div
|
|
399
|
+
key={message.id}
|
|
400
|
+
style={{
|
|
401
|
+
...styles.message,
|
|
402
|
+
...(message.role === 'user' ? styles.userMessage : styles.assistantMessage),
|
|
403
|
+
}}
|
|
404
|
+
>
|
|
405
|
+
<div style={styles.messageContent}>
|
|
406
|
+
{message.content}
|
|
407
|
+
{message.status === 'generating' && (
|
|
408
|
+
<span style={styles.spinner}>⟳</span>
|
|
409
|
+
)}
|
|
410
|
+
</div>
|
|
411
|
+
{message.status === 'success' && (
|
|
412
|
+
<div style={styles.successContainer}>
|
|
413
|
+
<div style={styles.successBadge}>✓ {editingOverride ? 'Updated' : 'Created'}</div>
|
|
414
|
+
<button
|
|
415
|
+
onClick={() => window.location.reload()}
|
|
416
|
+
style={styles.reloadButton}
|
|
417
|
+
>
|
|
418
|
+
Reload to Apply
|
|
419
|
+
</button>
|
|
420
|
+
</div>
|
|
421
|
+
)}
|
|
422
|
+
{message.status === 'error' && (
|
|
423
|
+
<div style={styles.errorBadge}>✕ Failed</div>
|
|
424
|
+
)}
|
|
425
|
+
</div>
|
|
426
|
+
))}
|
|
427
|
+
<button onClick={onClearHistory} style={styles.clearButton}>
|
|
428
|
+
Clear history
|
|
429
|
+
</button>
|
|
430
|
+
</>
|
|
431
|
+
)}
|
|
432
|
+
</div>
|
|
433
|
+
|
|
434
|
+
{/* Input */}
|
|
435
|
+
<form onSubmit={onSubmit} style={styles.inputForm}>
|
|
436
|
+
<input
|
|
437
|
+
type="text"
|
|
438
|
+
value={input}
|
|
439
|
+
onChange={(e) => onInputChange(e.target.value)}
|
|
440
|
+
placeholder="Describe your customization..."
|
|
441
|
+
disabled={isGenerating}
|
|
442
|
+
style={styles.input}
|
|
443
|
+
/>
|
|
444
|
+
<button
|
|
445
|
+
type="submit"
|
|
446
|
+
disabled={!input.trim() || isGenerating}
|
|
447
|
+
style={{
|
|
448
|
+
...styles.submitButton,
|
|
449
|
+
...((!input.trim() || isGenerating) ? styles.submitButtonDisabled : {}),
|
|
450
|
+
}}
|
|
451
|
+
>
|
|
452
|
+
{isGenerating ? '...' : '→'}
|
|
453
|
+
</button>
|
|
454
|
+
</form>
|
|
455
|
+
</div>
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
interface OverridesTabProps {
|
|
460
|
+
overrides: Array<{ id: string; name: string; description: string; enabled: boolean }>;
|
|
461
|
+
onToggle: (id: string, enabled: boolean) => void;
|
|
462
|
+
onDelete: (id: string) => void;
|
|
463
|
+
onRegenerate: (id: string) => void;
|
|
464
|
+
onEdit: (id: string) => void;
|
|
465
|
+
deletingId: string | null;
|
|
466
|
+
togglingId: string | null;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function OverridesTab({ overrides, onToggle, onDelete, onRegenerate, onEdit, deletingId, togglingId }: OverridesTabProps) {
|
|
470
|
+
const handleToggle = (override: { id: string; enabled: boolean }) => {
|
|
471
|
+
onToggle(override.id, !override.enabled);
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
if (overrides.length === 0) {
|
|
475
|
+
return (
|
|
476
|
+
<div style={styles.emptyState}>
|
|
477
|
+
<p style={styles.emptyTitle}>No overrides yet</p>
|
|
478
|
+
<p style={styles.emptyDescription}>
|
|
479
|
+
Use the Customize tab to create your first customization.
|
|
480
|
+
</p>
|
|
481
|
+
</div>
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return (
|
|
486
|
+
<div style={styles.overridesList}>
|
|
487
|
+
{overrides.map((override) => (
|
|
488
|
+
<div key={override.id} style={styles.overrideItem}>
|
|
489
|
+
<div style={styles.overrideInfo}>
|
|
490
|
+
<h3 style={styles.overrideName}>{override.name}</h3>
|
|
491
|
+
<p style={styles.overrideDescription}>{override.description}</p>
|
|
492
|
+
<div style={styles.overrideActions}>
|
|
493
|
+
<button
|
|
494
|
+
onClick={() => onEdit(override.id)}
|
|
495
|
+
style={styles.actionButton}
|
|
496
|
+
>
|
|
497
|
+
Edit
|
|
498
|
+
</button>
|
|
499
|
+
<button
|
|
500
|
+
onClick={() => onRegenerate(override.id)}
|
|
501
|
+
style={styles.actionButton}
|
|
502
|
+
>
|
|
503
|
+
Regenerate
|
|
504
|
+
</button>
|
|
505
|
+
<button
|
|
506
|
+
onClick={() => onDelete(override.id)}
|
|
507
|
+
disabled={deletingId === override.id}
|
|
508
|
+
style={{
|
|
509
|
+
...styles.actionButton,
|
|
510
|
+
...styles.deleteButton,
|
|
511
|
+
...(deletingId === override.id ? styles.actionButtonDisabled : {}),
|
|
512
|
+
}}
|
|
513
|
+
>
|
|
514
|
+
{deletingId === override.id ? 'Deleting...' : 'Delete'}
|
|
515
|
+
</button>
|
|
516
|
+
</div>
|
|
517
|
+
</div>
|
|
518
|
+
<button
|
|
519
|
+
onClick={() => handleToggle(override)}
|
|
520
|
+
disabled={togglingId === override.id}
|
|
521
|
+
style={{
|
|
522
|
+
...styles.toggle,
|
|
523
|
+
...(override.enabled ? styles.toggleOn : styles.toggleOff),
|
|
524
|
+
...(togglingId === override.id ? styles.toggleDisabled : {}),
|
|
525
|
+
}}
|
|
526
|
+
>
|
|
527
|
+
{togglingId === override.id ? '...' : override.enabled ? 'ON' : 'OFF'}
|
|
528
|
+
</button>
|
|
529
|
+
</div>
|
|
530
|
+
))}
|
|
531
|
+
</div>
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Icons
|
|
536
|
+
function SidekickIcon() {
|
|
537
|
+
return (
|
|
538
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
539
|
+
<path d="M12 2L2 7l10 5 10-5-10-5z" />
|
|
540
|
+
<path d="M2 17l10 5 10-5" />
|
|
541
|
+
<path d="M2 12l10 5 10-5" />
|
|
542
|
+
</svg>
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function CloseIcon() {
|
|
547
|
+
return (
|
|
548
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
549
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
550
|
+
</svg>
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function generateId(): string {
|
|
555
|
+
return Math.random().toString(36).substring(2, 11);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Styles
|
|
559
|
+
const styles: Record<string, React.CSSProperties> = {
|
|
560
|
+
trigger: {
|
|
561
|
+
position: 'fixed',
|
|
562
|
+
bottom: '24px',
|
|
563
|
+
right: '24px',
|
|
564
|
+
width: '56px',
|
|
565
|
+
height: '56px',
|
|
566
|
+
borderRadius: '50%',
|
|
567
|
+
backgroundColor: '#3B82F6',
|
|
568
|
+
color: 'white',
|
|
569
|
+
border: 'none',
|
|
570
|
+
cursor: 'pointer',
|
|
571
|
+
display: 'flex',
|
|
572
|
+
alignItems: 'center',
|
|
573
|
+
justifyContent: 'center',
|
|
574
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
575
|
+
zIndex: 9999,
|
|
576
|
+
},
|
|
577
|
+
overlay: {
|
|
578
|
+
position: 'fixed',
|
|
579
|
+
top: 0,
|
|
580
|
+
right: 0,
|
|
581
|
+
bottom: 0,
|
|
582
|
+
width: '400px',
|
|
583
|
+
zIndex: 9999,
|
|
584
|
+
boxShadow: '-4px 0 24px rgba(0, 0, 0, 0.15)',
|
|
585
|
+
overscrollBehavior: 'contain',
|
|
586
|
+
},
|
|
587
|
+
panel: {
|
|
588
|
+
height: '100%',
|
|
589
|
+
backgroundColor: 'white',
|
|
590
|
+
display: 'flex',
|
|
591
|
+
flexDirection: 'column',
|
|
592
|
+
},
|
|
593
|
+
header: {
|
|
594
|
+
display: 'flex',
|
|
595
|
+
alignItems: 'center',
|
|
596
|
+
justifyContent: 'space-between',
|
|
597
|
+
padding: '16px 20px',
|
|
598
|
+
borderBottom: '1px solid #E5E7EB',
|
|
599
|
+
},
|
|
600
|
+
title: {
|
|
601
|
+
margin: 0,
|
|
602
|
+
fontSize: '18px',
|
|
603
|
+
fontWeight: 600,
|
|
604
|
+
color: '#111827',
|
|
605
|
+
},
|
|
606
|
+
closeButton: {
|
|
607
|
+
background: 'none',
|
|
608
|
+
border: 'none',
|
|
609
|
+
cursor: 'pointer',
|
|
610
|
+
padding: '4px',
|
|
611
|
+
color: '#6B7280',
|
|
612
|
+
},
|
|
613
|
+
tabs: {
|
|
614
|
+
display: 'flex',
|
|
615
|
+
borderBottom: '1px solid #E5E7EB',
|
|
616
|
+
},
|
|
617
|
+
tab: {
|
|
618
|
+
flex: 1,
|
|
619
|
+
padding: '12px 16px',
|
|
620
|
+
background: 'none',
|
|
621
|
+
border: 'none',
|
|
622
|
+
cursor: 'pointer',
|
|
623
|
+
fontSize: '14px',
|
|
624
|
+
fontWeight: 500,
|
|
625
|
+
color: '#6B7280',
|
|
626
|
+
borderBottom: '2px solid transparent',
|
|
627
|
+
},
|
|
628
|
+
tabActive: {
|
|
629
|
+
color: '#3B82F6',
|
|
630
|
+
borderBottomColor: '#3B82F6',
|
|
631
|
+
},
|
|
632
|
+
content: {
|
|
633
|
+
flex: 1,
|
|
634
|
+
overflow: 'hidden',
|
|
635
|
+
display: 'flex',
|
|
636
|
+
flexDirection: 'column',
|
|
637
|
+
},
|
|
638
|
+
chatContainer: {
|
|
639
|
+
flex: 1,
|
|
640
|
+
display: 'flex',
|
|
641
|
+
flexDirection: 'column',
|
|
642
|
+
overflow: 'hidden',
|
|
643
|
+
},
|
|
644
|
+
messages: {
|
|
645
|
+
flex: 1,
|
|
646
|
+
overflowY: 'auto',
|
|
647
|
+
padding: '16px',
|
|
648
|
+
overscrollBehavior: 'contain',
|
|
649
|
+
},
|
|
650
|
+
emptyState: {
|
|
651
|
+
textAlign: 'center',
|
|
652
|
+
padding: '40px 20px',
|
|
653
|
+
},
|
|
654
|
+
emptyTitle: {
|
|
655
|
+
margin: '0 0 8px 0',
|
|
656
|
+
fontSize: '16px',
|
|
657
|
+
fontWeight: 600,
|
|
658
|
+
color: '#111827',
|
|
659
|
+
},
|
|
660
|
+
emptyDescription: {
|
|
661
|
+
margin: '0 0 24px 0',
|
|
662
|
+
fontSize: '14px',
|
|
663
|
+
color: '#6B7280',
|
|
664
|
+
},
|
|
665
|
+
examples: {
|
|
666
|
+
textAlign: 'left',
|
|
667
|
+
backgroundColor: '#F9FAFB',
|
|
668
|
+
borderRadius: '8px',
|
|
669
|
+
padding: '16px',
|
|
670
|
+
},
|
|
671
|
+
examplesTitle: {
|
|
672
|
+
margin: '0 0 8px 0',
|
|
673
|
+
fontSize: '13px',
|
|
674
|
+
fontWeight: 500,
|
|
675
|
+
color: '#374151',
|
|
676
|
+
},
|
|
677
|
+
examplesList: {
|
|
678
|
+
margin: 0,
|
|
679
|
+
padding: '0 0 0 20px',
|
|
680
|
+
fontSize: '13px',
|
|
681
|
+
color: '#6B7280',
|
|
682
|
+
lineHeight: 1.8,
|
|
683
|
+
},
|
|
684
|
+
message: {
|
|
685
|
+
marginBottom: '12px',
|
|
686
|
+
padding: '12px 16px',
|
|
687
|
+
borderRadius: '12px',
|
|
688
|
+
fontSize: '14px',
|
|
689
|
+
lineHeight: 1.5,
|
|
690
|
+
},
|
|
691
|
+
userMessage: {
|
|
692
|
+
backgroundColor: '#3B82F6',
|
|
693
|
+
color: 'white',
|
|
694
|
+
marginLeft: '40px',
|
|
695
|
+
},
|
|
696
|
+
assistantMessage: {
|
|
697
|
+
backgroundColor: '#F3F4F6',
|
|
698
|
+
color: '#111827',
|
|
699
|
+
marginRight: '40px',
|
|
700
|
+
},
|
|
701
|
+
messageContent: {
|
|
702
|
+
whiteSpace: 'pre-wrap',
|
|
703
|
+
},
|
|
704
|
+
spinner: {
|
|
705
|
+
display: 'inline-block',
|
|
706
|
+
animation: 'spin 1s linear infinite',
|
|
707
|
+
},
|
|
708
|
+
successContainer: {
|
|
709
|
+
marginTop: '12px',
|
|
710
|
+
display: 'flex',
|
|
711
|
+
flexDirection: 'column',
|
|
712
|
+
gap: '8px',
|
|
713
|
+
},
|
|
714
|
+
successBadge: {
|
|
715
|
+
fontSize: '12px',
|
|
716
|
+
color: '#059669',
|
|
717
|
+
fontWeight: 500,
|
|
718
|
+
},
|
|
719
|
+
reloadButton: {
|
|
720
|
+
padding: '8px 16px',
|
|
721
|
+
backgroundColor: '#059669',
|
|
722
|
+
color: 'white',
|
|
723
|
+
border: 'none',
|
|
724
|
+
borderRadius: '6px',
|
|
725
|
+
fontSize: '13px',
|
|
726
|
+
fontWeight: 500,
|
|
727
|
+
cursor: 'pointer',
|
|
728
|
+
},
|
|
729
|
+
errorBadge: {
|
|
730
|
+
marginTop: '8px',
|
|
731
|
+
fontSize: '12px',
|
|
732
|
+
color: '#DC2626',
|
|
733
|
+
fontWeight: 500,
|
|
734
|
+
},
|
|
735
|
+
clearButton: {
|
|
736
|
+
background: 'none',
|
|
737
|
+
border: 'none',
|
|
738
|
+
color: '#6B7280',
|
|
739
|
+
fontSize: '12px',
|
|
740
|
+
cursor: 'pointer',
|
|
741
|
+
padding: '8px',
|
|
742
|
+
textAlign: 'center',
|
|
743
|
+
width: '100%',
|
|
744
|
+
},
|
|
745
|
+
inputForm: {
|
|
746
|
+
display: 'flex',
|
|
747
|
+
gap: '8px',
|
|
748
|
+
padding: '16px',
|
|
749
|
+
borderTop: '1px solid #E5E7EB',
|
|
750
|
+
},
|
|
751
|
+
input: {
|
|
752
|
+
flex: 1,
|
|
753
|
+
padding: '12px 16px',
|
|
754
|
+
border: '1px solid #E5E7EB',
|
|
755
|
+
borderRadius: '8px',
|
|
756
|
+
fontSize: '14px',
|
|
757
|
+
outline: 'none',
|
|
758
|
+
},
|
|
759
|
+
submitButton: {
|
|
760
|
+
padding: '12px 20px',
|
|
761
|
+
backgroundColor: '#3B82F6',
|
|
762
|
+
color: 'white',
|
|
763
|
+
border: 'none',
|
|
764
|
+
borderRadius: '8px',
|
|
765
|
+
fontSize: '16px',
|
|
766
|
+
cursor: 'pointer',
|
|
767
|
+
fontWeight: 600,
|
|
768
|
+
},
|
|
769
|
+
submitButtonDisabled: {
|
|
770
|
+
backgroundColor: '#93C5FD',
|
|
771
|
+
cursor: 'not-allowed',
|
|
772
|
+
},
|
|
773
|
+
overridesList: {
|
|
774
|
+
padding: '16px',
|
|
775
|
+
flex: 1,
|
|
776
|
+
overflowY: 'auto',
|
|
777
|
+
overscrollBehavior: 'contain',
|
|
778
|
+
},
|
|
779
|
+
overrideItem: {
|
|
780
|
+
display: 'flex',
|
|
781
|
+
alignItems: 'center',
|
|
782
|
+
justifyContent: 'space-between',
|
|
783
|
+
padding: '16px',
|
|
784
|
+
backgroundColor: '#F9FAFB',
|
|
785
|
+
borderRadius: '8px',
|
|
786
|
+
marginBottom: '12px',
|
|
787
|
+
},
|
|
788
|
+
overrideInfo: {
|
|
789
|
+
flex: 1,
|
|
790
|
+
marginRight: '16px',
|
|
791
|
+
},
|
|
792
|
+
overrideName: {
|
|
793
|
+
margin: '0 0 4px 0',
|
|
794
|
+
fontSize: '14px',
|
|
795
|
+
fontWeight: 600,
|
|
796
|
+
color: '#111827',
|
|
797
|
+
},
|
|
798
|
+
overrideDescription: {
|
|
799
|
+
margin: 0,
|
|
800
|
+
fontSize: '13px',
|
|
801
|
+
color: '#6B7280',
|
|
802
|
+
},
|
|
803
|
+
toggle: {
|
|
804
|
+
padding: '6px 12px',
|
|
805
|
+
borderRadius: '4px',
|
|
806
|
+
border: 'none',
|
|
807
|
+
fontSize: '12px',
|
|
808
|
+
fontWeight: 600,
|
|
809
|
+
cursor: 'pointer',
|
|
810
|
+
},
|
|
811
|
+
toggleOn: {
|
|
812
|
+
backgroundColor: '#059669',
|
|
813
|
+
color: 'white',
|
|
814
|
+
},
|
|
815
|
+
toggleOff: {
|
|
816
|
+
backgroundColor: '#E5E7EB',
|
|
817
|
+
color: '#6B7280',
|
|
818
|
+
},
|
|
819
|
+
toggleDisabled: {
|
|
820
|
+
opacity: 0.5,
|
|
821
|
+
cursor: 'not-allowed',
|
|
822
|
+
},
|
|
823
|
+
overrideActions: {
|
|
824
|
+
display: 'flex',
|
|
825
|
+
gap: '8px',
|
|
826
|
+
marginTop: '8px',
|
|
827
|
+
},
|
|
828
|
+
actionButton: {
|
|
829
|
+
padding: '4px 8px',
|
|
830
|
+
fontSize: '11px',
|
|
831
|
+
fontWeight: 500,
|
|
832
|
+
border: '1px solid #E5E7EB',
|
|
833
|
+
borderRadius: '4px',
|
|
834
|
+
backgroundColor: 'white',
|
|
835
|
+
color: '#6B7280',
|
|
836
|
+
cursor: 'pointer',
|
|
837
|
+
},
|
|
838
|
+
actionButtonDisabled: {
|
|
839
|
+
opacity: 0.5,
|
|
840
|
+
cursor: 'not-allowed',
|
|
841
|
+
},
|
|
842
|
+
deleteButton: {
|
|
843
|
+
color: '#DC2626',
|
|
844
|
+
borderColor: '#FCA5A5',
|
|
845
|
+
},
|
|
846
|
+
editHeader: {
|
|
847
|
+
display: 'flex',
|
|
848
|
+
alignItems: 'center',
|
|
849
|
+
justifyContent: 'space-between',
|
|
850
|
+
padding: '8px 16px',
|
|
851
|
+
backgroundColor: '#EFF6FF',
|
|
852
|
+
borderBottom: '1px solid #BFDBFE',
|
|
853
|
+
},
|
|
854
|
+
editHeaderText: {
|
|
855
|
+
fontSize: '13px',
|
|
856
|
+
fontWeight: 600,
|
|
857
|
+
color: '#1D4ED8',
|
|
858
|
+
},
|
|
859
|
+
editHeaderClose: {
|
|
860
|
+
background: 'none',
|
|
861
|
+
border: 'none',
|
|
862
|
+
cursor: 'pointer',
|
|
863
|
+
fontSize: '14px',
|
|
864
|
+
color: '#1D4ED8',
|
|
865
|
+
padding: '2px 6px',
|
|
866
|
+
borderRadius: '4px',
|
|
867
|
+
},
|
|
868
|
+
};
|