@tpitre/story-ui 2.3.0 → 2.3.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.
- package/package.json +1 -1
- package/templates/production-app/src/App.tsx +474 -69
package/package.json
CHANGED
|
@@ -38,6 +38,28 @@ interface Conversation {
|
|
|
38
38
|
updatedAt: number;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
interface ProviderOption {
|
|
42
|
+
id: string;
|
|
43
|
+
name: string;
|
|
44
|
+
isDefault?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface ModelOption {
|
|
48
|
+
id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
provider?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ServerConfig {
|
|
54
|
+
providers: ProviderOption[];
|
|
55
|
+
currentProvider: string;
|
|
56
|
+
models: ModelOption[];
|
|
57
|
+
currentModel: string;
|
|
58
|
+
isConfigured: boolean;
|
|
59
|
+
loading: boolean;
|
|
60
|
+
error: string | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
41
63
|
// ============================================================================
|
|
42
64
|
// CONSTANTS & CONFIG
|
|
43
65
|
// ============================================================================
|
|
@@ -85,11 +107,16 @@ const useLocalStorage = <T,>(key: string, initialValue: T): [T, React.Dispatch<R
|
|
|
85
107
|
}
|
|
86
108
|
});
|
|
87
109
|
|
|
88
|
-
|
|
89
|
-
|
|
110
|
+
// Use a ref to always have access to the latest value for functional updates
|
|
111
|
+
const storedValueRef = useRef(storedValue);
|
|
112
|
+
storedValueRef.current = storedValue;
|
|
113
|
+
|
|
114
|
+
const setValue: React.Dispatch<React.SetStateAction<T>> = useCallback((value) => {
|
|
115
|
+
const valueToStore = value instanceof Function ? value(storedValueRef.current) : value;
|
|
116
|
+
storedValueRef.current = valueToStore;
|
|
90
117
|
setStoredValue(valueToStore);
|
|
91
118
|
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
|
92
|
-
};
|
|
119
|
+
}, [key]);
|
|
93
120
|
|
|
94
121
|
return [storedValue, setValue];
|
|
95
122
|
};
|
|
@@ -133,6 +160,95 @@ const useResizable = (initialWidth: number, minWidth: number, maxWidth: number)
|
|
|
133
160
|
return { width, startResize };
|
|
134
161
|
};
|
|
135
162
|
|
|
163
|
+
// Default models for fallback
|
|
164
|
+
const DEFAULT_MODELS: Record<string, ModelOption[]> = {
|
|
165
|
+
claude: [
|
|
166
|
+
{ id: 'claude-sonnet-4-5-20250929', name: 'Claude Sonnet 4.5', provider: 'claude' },
|
|
167
|
+
{ id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', provider: 'claude' },
|
|
168
|
+
{ id: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet', provider: 'claude' },
|
|
169
|
+
],
|
|
170
|
+
openai: [
|
|
171
|
+
{ id: 'gpt-4o', name: 'GPT-4o', provider: 'openai' },
|
|
172
|
+
{ id: 'gpt-4o-mini', name: 'GPT-4o Mini', provider: 'openai' },
|
|
173
|
+
{ id: 'gpt-4-turbo', name: 'GPT-4 Turbo', provider: 'openai' },
|
|
174
|
+
],
|
|
175
|
+
gemini: [
|
|
176
|
+
{ id: 'gemini-2.0-flash', name: 'Gemini 2.0 Flash', provider: 'gemini' },
|
|
177
|
+
{ id: 'gemini-1.5-pro', name: 'Gemini 1.5 Pro', provider: 'gemini' },
|
|
178
|
+
{ id: 'gemini-1.5-flash', name: 'Gemini 1.5 Flash', provider: 'gemini' },
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const useServerConfig = () => {
|
|
183
|
+
const [config, setConfig] = useState<ServerConfig>({
|
|
184
|
+
providers: [],
|
|
185
|
+
currentProvider: 'claude',
|
|
186
|
+
models: DEFAULT_MODELS.claude,
|
|
187
|
+
currentModel: DEFAULT_MODELS.claude[0].id,
|
|
188
|
+
isConfigured: false,
|
|
189
|
+
loading: true,
|
|
190
|
+
error: null,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const fetchConfig = useCallback(async () => {
|
|
194
|
+
setConfig(prev => ({ ...prev, loading: true, error: null }));
|
|
195
|
+
try {
|
|
196
|
+
const response = await fetch(`${SERVER_URL}/story-ui/providers`);
|
|
197
|
+
if (response.ok) {
|
|
198
|
+
const data = await response.json();
|
|
199
|
+
const providers = data.providers || [];
|
|
200
|
+
const defaultProvider = providers.find((p: ProviderOption) => p.isDefault)?.id || providers[0]?.id || 'claude';
|
|
201
|
+
const models = DEFAULT_MODELS[defaultProvider] || DEFAULT_MODELS.claude;
|
|
202
|
+
setConfig({
|
|
203
|
+
providers,
|
|
204
|
+
currentProvider: defaultProvider,
|
|
205
|
+
models,
|
|
206
|
+
currentModel: models[0]?.id || '',
|
|
207
|
+
isConfigured: true,
|
|
208
|
+
loading: false,
|
|
209
|
+
error: null,
|
|
210
|
+
});
|
|
211
|
+
} else {
|
|
212
|
+
throw new Error('Failed to fetch providers');
|
|
213
|
+
}
|
|
214
|
+
} catch (err) {
|
|
215
|
+
// Fallback to default Claude config
|
|
216
|
+
setConfig({
|
|
217
|
+
providers: [{ id: 'claude', name: 'Claude (Anthropic)', isDefault: true }],
|
|
218
|
+
currentProvider: 'claude',
|
|
219
|
+
models: DEFAULT_MODELS.claude,
|
|
220
|
+
currentModel: DEFAULT_MODELS.claude[0].id,
|
|
221
|
+
isConfigured: true,
|
|
222
|
+
loading: false,
|
|
223
|
+
error: null,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}, []);
|
|
227
|
+
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
fetchConfig();
|
|
230
|
+
}, [fetchConfig]);
|
|
231
|
+
|
|
232
|
+
const changeProvider = useCallback((providerId: string) => {
|
|
233
|
+
const models = DEFAULT_MODELS[providerId] || DEFAULT_MODELS.claude;
|
|
234
|
+
setConfig(prev => ({
|
|
235
|
+
...prev,
|
|
236
|
+
currentProvider: providerId,
|
|
237
|
+
models,
|
|
238
|
+
currentModel: models[0]?.id || '',
|
|
239
|
+
}));
|
|
240
|
+
}, []);
|
|
241
|
+
|
|
242
|
+
const changeModel = useCallback((modelId: string) => {
|
|
243
|
+
setConfig(prev => ({
|
|
244
|
+
...prev,
|
|
245
|
+
currentModel: modelId,
|
|
246
|
+
}));
|
|
247
|
+
}, []);
|
|
248
|
+
|
|
249
|
+
return { ...config, refetch: fetchConfig, changeProvider, changeModel };
|
|
250
|
+
};
|
|
251
|
+
|
|
136
252
|
// ============================================================================
|
|
137
253
|
// ICON COMPONENTS
|
|
138
254
|
// ============================================================================
|
|
@@ -193,12 +309,120 @@ const Icons = {
|
|
|
193
309
|
<path d="M18 6L6 18M6 6l12 12" />
|
|
194
310
|
</svg>
|
|
195
311
|
),
|
|
312
|
+
ChevronDown: () => (
|
|
313
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
314
|
+
<path d="M6 9l6 6 6-6" />
|
|
315
|
+
</svg>
|
|
316
|
+
),
|
|
196
317
|
};
|
|
197
318
|
|
|
198
319
|
// ============================================================================
|
|
199
320
|
// SUB-COMPONENTS
|
|
200
321
|
// ============================================================================
|
|
201
322
|
|
|
323
|
+
// Dropdown component for provider/model selection
|
|
324
|
+
const Dropdown: React.FC<{
|
|
325
|
+
label: string;
|
|
326
|
+
value: string;
|
|
327
|
+
options: { id: string; name: string }[];
|
|
328
|
+
onChange: (value: string) => void;
|
|
329
|
+
disabled?: boolean;
|
|
330
|
+
}> = ({ label, value, options, onChange, disabled }) => {
|
|
331
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
332
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
333
|
+
const selectedOption = options.find(o => o.id === value);
|
|
334
|
+
|
|
335
|
+
useEffect(() => {
|
|
336
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
337
|
+
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
338
|
+
setIsOpen(false);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
342
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
343
|
+
}, []);
|
|
344
|
+
|
|
345
|
+
return (
|
|
346
|
+
<div ref={ref} style={{ position: 'relative', width: '100%' }}>
|
|
347
|
+
<button
|
|
348
|
+
onClick={() => !disabled && setIsOpen(!isOpen)}
|
|
349
|
+
disabled={disabled}
|
|
350
|
+
style={{
|
|
351
|
+
display: 'flex',
|
|
352
|
+
alignItems: 'center',
|
|
353
|
+
justifyContent: 'space-between',
|
|
354
|
+
width: '100%',
|
|
355
|
+
padding: '8px 10px',
|
|
356
|
+
background: THEME.bgElevated,
|
|
357
|
+
border: `1px solid ${THEME.border}`,
|
|
358
|
+
borderRadius: '6px',
|
|
359
|
+
color: disabled ? THEME.textSubtle : THEME.text,
|
|
360
|
+
fontSize: '12px',
|
|
361
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
362
|
+
transition: 'all 0.2s',
|
|
363
|
+
opacity: disabled ? 0.6 : 1,
|
|
364
|
+
}}
|
|
365
|
+
>
|
|
366
|
+
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: '2px' }}>
|
|
367
|
+
<span style={{ color: THEME.textSubtle, fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.5px' }}>
|
|
368
|
+
{label}
|
|
369
|
+
</span>
|
|
370
|
+
<span style={{ fontWeight: 500 }}>{selectedOption?.name || 'Select'}</span>
|
|
371
|
+
</div>
|
|
372
|
+
<Icons.ChevronDown />
|
|
373
|
+
</button>
|
|
374
|
+
{isOpen && !disabled && (
|
|
375
|
+
<div
|
|
376
|
+
style={{
|
|
377
|
+
position: 'absolute',
|
|
378
|
+
bottom: '100%',
|
|
379
|
+
left: 0,
|
|
380
|
+
right: 0,
|
|
381
|
+
marginBottom: '4px',
|
|
382
|
+
maxHeight: '200px',
|
|
383
|
+
overflowY: 'auto',
|
|
384
|
+
background: THEME.bgElevated,
|
|
385
|
+
border: `1px solid ${THEME.border}`,
|
|
386
|
+
borderRadius: '6px',
|
|
387
|
+
boxShadow: '0 -4px 12px rgba(0,0,0,0.3)',
|
|
388
|
+
zIndex: 100,
|
|
389
|
+
}}
|
|
390
|
+
>
|
|
391
|
+
{options.map(option => (
|
|
392
|
+
<button
|
|
393
|
+
key={option.id}
|
|
394
|
+
onClick={() => {
|
|
395
|
+
onChange(option.id);
|
|
396
|
+
setIsOpen(false);
|
|
397
|
+
}}
|
|
398
|
+
style={{
|
|
399
|
+
display: 'block',
|
|
400
|
+
width: '100%',
|
|
401
|
+
padding: '8px 10px',
|
|
402
|
+
background: option.id === value ? THEME.accentMuted : 'transparent',
|
|
403
|
+
border: 'none',
|
|
404
|
+
textAlign: 'left',
|
|
405
|
+
color: THEME.text,
|
|
406
|
+
fontSize: '12px',
|
|
407
|
+
cursor: 'pointer',
|
|
408
|
+
transition: 'background 0.15s',
|
|
409
|
+
}}
|
|
410
|
+
onMouseEnter={e => {
|
|
411
|
+
if (option.id !== value) e.currentTarget.style.background = THEME.bgHover;
|
|
412
|
+
}}
|
|
413
|
+
onMouseLeave={e => {
|
|
414
|
+
e.currentTarget.style.background = option.id === value ? THEME.accentMuted : 'transparent';
|
|
415
|
+
}}
|
|
416
|
+
>
|
|
417
|
+
{option.name}
|
|
418
|
+
</button>
|
|
419
|
+
))}
|
|
420
|
+
</div>
|
|
421
|
+
)}
|
|
422
|
+
</div>
|
|
423
|
+
);
|
|
424
|
+
};
|
|
425
|
+
|
|
202
426
|
const LoadingDots: React.FC = () => (
|
|
203
427
|
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
|
|
204
428
|
{[0, 1, 2].map(i => (
|
|
@@ -390,6 +614,9 @@ const CodeViewer: React.FC<{ code: string }> = ({ code }) => {
|
|
|
390
614
|
// ============================================================================
|
|
391
615
|
|
|
392
616
|
const App: React.FC = () => {
|
|
617
|
+
// Server configuration (providers, models)
|
|
618
|
+
const serverConfig = useServerConfig();
|
|
619
|
+
|
|
393
620
|
const [conversations, setConversations] = useLocalStorage<Conversation[]>('storyui_conversations', []);
|
|
394
621
|
const [activeConversationId, setActiveConversationId] = useState<string | null>(null);
|
|
395
622
|
const [inputValue, setInputValue] = useState('');
|
|
@@ -405,45 +632,57 @@ const App: React.FC = () => {
|
|
|
405
632
|
|
|
406
633
|
const activeConversation = conversations.find(c => c.id === activeConversationId);
|
|
407
634
|
|
|
635
|
+
// Filter out empty conversations for display (deferred creation)
|
|
636
|
+
const displayConversations = conversations.filter(c => c.messages.length > 0);
|
|
637
|
+
|
|
408
638
|
// Auto-scroll messages
|
|
409
639
|
useEffect(() => {
|
|
410
640
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
411
641
|
}, [activeConversation?.messages]);
|
|
412
642
|
|
|
413
|
-
// Initialize
|
|
643
|
+
// Initialize - select existing conversation or stay in "new chat" mode
|
|
414
644
|
useEffect(() => {
|
|
415
|
-
if
|
|
416
|
-
|
|
417
|
-
|
|
645
|
+
// Only select an existing conversation if we don't have one selected
|
|
646
|
+
// and there are conversations with messages
|
|
647
|
+
if (!activeConversationId && displayConversations.length > 0) {
|
|
648
|
+
setActiveConversationId(displayConversations[0].id);
|
|
649
|
+
const lastMsgWithCode = [...displayConversations[0].messages].reverse().find(m => m.generatedCode);
|
|
418
650
|
setPreviewCode(lastMsgWithCode?.generatedCode || null);
|
|
419
|
-
} else if (conversations.length === 0) {
|
|
420
|
-
createNewConversation();
|
|
421
651
|
}
|
|
422
|
-
|
|
652
|
+
// Don't create an empty conversation - we'll create one when user sends first message
|
|
653
|
+
}, [displayConversations.length, activeConversationId]);
|
|
654
|
+
|
|
655
|
+
// Start a new chat - just clear state, don't create empty conversation
|
|
656
|
+
const startNewChat = useCallback(() => {
|
|
657
|
+
setActiveConversationId(null);
|
|
658
|
+
setPreviewCode(null);
|
|
659
|
+
setImages([]);
|
|
660
|
+
inputRef.current?.focus();
|
|
661
|
+
}, []);
|
|
423
662
|
|
|
424
|
-
|
|
663
|
+
// Actually create a conversation when first message is sent
|
|
664
|
+
const createConversationWithMessage = useCallback((message: Message): string => {
|
|
425
665
|
const newConversation: Conversation = {
|
|
426
666
|
id: generateId(),
|
|
427
|
-
title:
|
|
428
|
-
messages: [],
|
|
667
|
+
title: message.content.substring(0, 40),
|
|
668
|
+
messages: [message],
|
|
429
669
|
createdAt: Date.now(),
|
|
430
670
|
updatedAt: Date.now(),
|
|
431
671
|
};
|
|
432
672
|
setConversations(prev => [newConversation, ...prev]);
|
|
433
|
-
|
|
434
|
-
setPreviewCode(null);
|
|
435
|
-
setImages([]);
|
|
436
|
-
inputRef.current?.focus();
|
|
673
|
+
return newConversation.id;
|
|
437
674
|
}, [setConversations]);
|
|
438
675
|
|
|
439
676
|
const deleteConversation = (id: string) => {
|
|
440
677
|
setConversations(prev => prev.filter(c => c.id !== id));
|
|
441
678
|
if (activeConversationId === id) {
|
|
442
|
-
const remaining =
|
|
679
|
+
const remaining = displayConversations.filter(c => c.id !== id);
|
|
443
680
|
if (remaining.length > 0) {
|
|
444
681
|
setActiveConversationId(remaining[0].id);
|
|
445
682
|
} else {
|
|
446
|
-
|
|
683
|
+
// Go to "new chat" mode instead of creating empty conversation
|
|
684
|
+
setActiveConversationId(null);
|
|
685
|
+
setPreviewCode(null);
|
|
447
686
|
}
|
|
448
687
|
}
|
|
449
688
|
};
|
|
@@ -522,6 +761,8 @@ MODIFICATION RULES:
|
|
|
522
761
|
3. Preserve existing styling, layout, and components not mentioned
|
|
523
762
|
4. Output the COMPLETE modified JSX (not just the changed parts)
|
|
524
763
|
|
|
764
|
+
IMPORTANT: User requests OVERRIDE design system defaults. If the user asks to change colors, styling, or any other aspect, follow their request exactly - even if it differs from the design system guidelines above.
|
|
765
|
+
|
|
525
766
|
OUTPUT: Start immediately with < and output only the complete modified JSX.`;
|
|
526
767
|
} else {
|
|
527
768
|
// New generation prompt
|
|
@@ -548,6 +789,7 @@ OUTPUT: Start immediately with < and output only JSX.`;
|
|
|
548
789
|
messages: conversationHistory,
|
|
549
790
|
systemPrompt,
|
|
550
791
|
prefillAssistant,
|
|
792
|
+
model: serverConfig.currentModel,
|
|
551
793
|
maxTokens: 4096,
|
|
552
794
|
images: imageAttachments.map(img => ({
|
|
553
795
|
type: img.type,
|
|
@@ -588,7 +830,7 @@ OUTPUT: Start immediately with < and output only JSX.`;
|
|
|
588
830
|
};
|
|
589
831
|
|
|
590
832
|
const sendMessage = async () => {
|
|
591
|
-
if (!inputValue.trim() ||
|
|
833
|
+
if (!inputValue.trim() || isGenerating) return;
|
|
592
834
|
|
|
593
835
|
const userMessage: Message = {
|
|
594
836
|
id: generateId(),
|
|
@@ -598,17 +840,26 @@ OUTPUT: Start immediately with < and output only JSX.`;
|
|
|
598
840
|
images: images.length > 0 ? [...images] : undefined,
|
|
599
841
|
};
|
|
600
842
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
843
|
+
// Determine if we need to create a new conversation or add to existing
|
|
844
|
+
let conversationId = activeConversationId;
|
|
845
|
+
|
|
846
|
+
if (!conversationId) {
|
|
847
|
+
// Create new conversation with the first message
|
|
848
|
+
conversationId = createConversationWithMessage(userMessage);
|
|
849
|
+
setActiveConversationId(conversationId);
|
|
850
|
+
} else {
|
|
851
|
+
// Add message to existing conversation
|
|
852
|
+
setConversations(prev => prev.map(conv => {
|
|
853
|
+
if (conv.id === conversationId) {
|
|
854
|
+
return {
|
|
855
|
+
...conv,
|
|
856
|
+
messages: [...conv.messages, userMessage],
|
|
857
|
+
updatedAt: Date.now(),
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
return conv;
|
|
861
|
+
}));
|
|
862
|
+
}
|
|
612
863
|
|
|
613
864
|
const currentImages = [...images];
|
|
614
865
|
setInputValue('');
|
|
@@ -627,7 +878,7 @@ OUTPUT: Start immediately with < and output only JSX.`;
|
|
|
627
878
|
};
|
|
628
879
|
|
|
629
880
|
setConversations(prev => prev.map(conv => {
|
|
630
|
-
if (conv.id ===
|
|
881
|
+
if (conv.id === conversationId) {
|
|
631
882
|
return {
|
|
632
883
|
...conv,
|
|
633
884
|
messages: [...conv.messages, assistantMessage],
|
|
@@ -650,7 +901,7 @@ OUTPUT: Start immediately with < and output only JSX.`;
|
|
|
650
901
|
};
|
|
651
902
|
|
|
652
903
|
setConversations(prev => prev.map(conv => {
|
|
653
|
-
if (conv.id ===
|
|
904
|
+
if (conv.id === conversationId) {
|
|
654
905
|
return {
|
|
655
906
|
...conv,
|
|
656
907
|
messages: [...conv.messages, errorMessage],
|
|
@@ -740,7 +991,7 @@ OUTPUT: Start immediately with < and output only JSX.`;
|
|
|
740
991
|
{/* New Chat Button */}
|
|
741
992
|
<div style={{ padding: sidebarCollapsed ? '8px' : '12px' }}>
|
|
742
993
|
<button
|
|
743
|
-
onClick={
|
|
994
|
+
onClick={startNewChat}
|
|
744
995
|
style={{
|
|
745
996
|
width: '100%',
|
|
746
997
|
padding: sidebarCollapsed ? '10px' : '10px 14px',
|
|
@@ -765,7 +1016,7 @@ OUTPUT: Start immediately with < and output only JSX.`;
|
|
|
765
1016
|
{/* Conversation List */}
|
|
766
1017
|
{!sidebarCollapsed && (
|
|
767
1018
|
<div style={{ flex: 1, overflow: 'auto', padding: '8px 12px' }}>
|
|
768
|
-
{
|
|
1019
|
+
{displayConversations.map(conv => (
|
|
769
1020
|
<div
|
|
770
1021
|
key={conv.id}
|
|
771
1022
|
onClick={() => {
|
|
@@ -815,15 +1066,62 @@ OUTPUT: Start immediately with < and output only JSX.`;
|
|
|
815
1066
|
</div>
|
|
816
1067
|
)}
|
|
817
1068
|
|
|
818
|
-
{/*
|
|
1069
|
+
{/* Sidebar Footer - Provider/Model Info */}
|
|
819
1070
|
{!sidebarCollapsed && (
|
|
820
1071
|
<div style={{
|
|
821
1072
|
padding: '12px',
|
|
822
1073
|
borderTop: `1px solid ${THEME.border}`,
|
|
823
|
-
|
|
824
|
-
|
|
1074
|
+
display: 'flex',
|
|
1075
|
+
flexDirection: 'column',
|
|
1076
|
+
gap: '6px',
|
|
825
1077
|
}}>
|
|
826
|
-
|
|
1078
|
+
{/* Provider Label */}
|
|
1079
|
+
<div style={{ fontSize: '12px', color: THEME.textMuted }}>
|
|
1080
|
+
<span style={{ color: THEME.textSubtle, textTransform: 'uppercase', fontSize: '10px', letterSpacing: '0.5px' }}>Provider: </span>
|
|
1081
|
+
<span style={{ color: THEME.text, fontWeight: 500 }}>
|
|
1082
|
+
{serverConfig.providers.find(p => p.id === serverConfig.currentProvider)?.name || 'Claude (Anthropic)'}
|
|
1083
|
+
</span>
|
|
1084
|
+
</div>
|
|
1085
|
+
|
|
1086
|
+
{/* Model Dropdown */}
|
|
1087
|
+
<div style={{ fontSize: '12px', color: THEME.textMuted }}>
|
|
1088
|
+
<span style={{ color: THEME.textSubtle, textTransform: 'uppercase', fontSize: '10px', letterSpacing: '0.5px' }}>Model</span>
|
|
1089
|
+
</div>
|
|
1090
|
+
<select
|
|
1091
|
+
value={serverConfig.currentModel}
|
|
1092
|
+
onChange={(e) => serverConfig.changeModel(e.target.value)}
|
|
1093
|
+
disabled={serverConfig.loading}
|
|
1094
|
+
style={{
|
|
1095
|
+
width: '100%',
|
|
1096
|
+
padding: '8px 10px',
|
|
1097
|
+
background: THEME.bgElevated,
|
|
1098
|
+
border: `1px solid ${THEME.border}`,
|
|
1099
|
+
borderRadius: '6px',
|
|
1100
|
+
color: THEME.text,
|
|
1101
|
+
fontSize: '12px',
|
|
1102
|
+
cursor: 'pointer',
|
|
1103
|
+
outline: 'none',
|
|
1104
|
+
appearance: 'none',
|
|
1105
|
+
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2371717a' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E")`,
|
|
1106
|
+
backgroundRepeat: 'no-repeat',
|
|
1107
|
+
backgroundPosition: 'right 8px center',
|
|
1108
|
+
paddingRight: '28px',
|
|
1109
|
+
}}
|
|
1110
|
+
>
|
|
1111
|
+
{serverConfig.models.map(model => (
|
|
1112
|
+
<option key={model.id} value={model.id}>{model.id}</option>
|
|
1113
|
+
))}
|
|
1114
|
+
</select>
|
|
1115
|
+
|
|
1116
|
+
{/* Components Count */}
|
|
1117
|
+
<div style={{
|
|
1118
|
+
display: 'flex',
|
|
1119
|
+
alignItems: 'center',
|
|
1120
|
+
gap: '6px',
|
|
1121
|
+
fontSize: '11px',
|
|
1122
|
+
color: THEME.textSubtle,
|
|
1123
|
+
paddingTop: '4px',
|
|
1124
|
+
}}>
|
|
827
1125
|
<div style={{
|
|
828
1126
|
width: '6px',
|
|
829
1127
|
height: '6px',
|
|
@@ -847,7 +1145,7 @@ OUTPUT: Start immediately with < and output only JSX.`;
|
|
|
847
1145
|
}}>
|
|
848
1146
|
{/* Messages */}
|
|
849
1147
|
<div style={{ flex: 1, overflow: 'auto', padding: '16px' }}>
|
|
850
|
-
{activeConversation
|
|
1148
|
+
{(!activeConversation || activeConversation.messages.length === 0) && (
|
|
851
1149
|
<div style={{
|
|
852
1150
|
padding: '32px 16px',
|
|
853
1151
|
textAlign: 'center',
|
|
@@ -970,19 +1268,68 @@ OUTPUT: Start immediately with < and output only JSX.`;
|
|
|
970
1268
|
<div ref={messagesEndRef} />
|
|
971
1269
|
</div>
|
|
972
1270
|
|
|
973
|
-
{/* Input Area */}
|
|
1271
|
+
{/* Input Area - ChatGPT/Lovable Style */}
|
|
974
1272
|
<div style={{
|
|
975
1273
|
padding: '16px',
|
|
976
1274
|
borderTop: `1px solid ${THEME.border}`,
|
|
977
1275
|
background: THEME.bgSurface,
|
|
978
1276
|
}}>
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1277
|
+
{/* Image thumbnails if any */}
|
|
1278
|
+
{images.length > 0 && (
|
|
1279
|
+
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginBottom: '12px' }}>
|
|
1280
|
+
{images.map(img => (
|
|
1281
|
+
<div
|
|
1282
|
+
key={img.id}
|
|
1283
|
+
style={{
|
|
1284
|
+
position: 'relative',
|
|
1285
|
+
width: '64px',
|
|
1286
|
+
height: '64px',
|
|
1287
|
+
borderRadius: '8px',
|
|
1288
|
+
overflow: 'hidden',
|
|
1289
|
+
border: `1px solid ${THEME.border}`,
|
|
1290
|
+
}}
|
|
1291
|
+
>
|
|
1292
|
+
<img
|
|
1293
|
+
src={img.data}
|
|
1294
|
+
alt={img.name}
|
|
1295
|
+
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
|
1296
|
+
/>
|
|
1297
|
+
<button
|
|
1298
|
+
onClick={() => setImages(images.filter(i => i.id !== img.id))}
|
|
1299
|
+
style={{
|
|
1300
|
+
position: 'absolute',
|
|
1301
|
+
top: '2px',
|
|
1302
|
+
right: '2px',
|
|
1303
|
+
width: '18px',
|
|
1304
|
+
height: '18px',
|
|
1305
|
+
borderRadius: '50%',
|
|
1306
|
+
background: 'rgba(0,0,0,0.7)',
|
|
1307
|
+
border: 'none',
|
|
1308
|
+
color: '#fff',
|
|
1309
|
+
cursor: 'pointer',
|
|
1310
|
+
display: 'flex',
|
|
1311
|
+
alignItems: 'center',
|
|
1312
|
+
justifyContent: 'center',
|
|
1313
|
+
fontSize: '10px',
|
|
1314
|
+
}}
|
|
1315
|
+
>
|
|
1316
|
+
<Icons.X />
|
|
1317
|
+
</button>
|
|
1318
|
+
</div>
|
|
1319
|
+
))}
|
|
1320
|
+
</div>
|
|
1321
|
+
)}
|
|
984
1322
|
|
|
985
|
-
|
|
1323
|
+
{/* Combined input container matching reference */}
|
|
1324
|
+
<div style={{
|
|
1325
|
+
display: 'flex',
|
|
1326
|
+
flexDirection: 'column',
|
|
1327
|
+
background: THEME.bgElevated,
|
|
1328
|
+
border: `1px solid ${THEME.border}`,
|
|
1329
|
+
borderRadius: '12px',
|
|
1330
|
+
overflow: 'hidden',
|
|
1331
|
+
}}>
|
|
1332
|
+
{/* Text input area */}
|
|
986
1333
|
<textarea
|
|
987
1334
|
ref={inputRef}
|
|
988
1335
|
value={inputValue}
|
|
@@ -991,37 +1338,95 @@ OUTPUT: Start immediately with < and output only JSX.`;
|
|
|
991
1338
|
placeholder="Describe the component you want to create..."
|
|
992
1339
|
disabled={isGenerating}
|
|
993
1340
|
style={{
|
|
994
|
-
|
|
995
|
-
padding: '
|
|
996
|
-
background:
|
|
997
|
-
border:
|
|
998
|
-
borderRadius: '10px',
|
|
1341
|
+
width: '100%',
|
|
1342
|
+
padding: '14px 16px',
|
|
1343
|
+
background: 'transparent',
|
|
1344
|
+
border: 'none',
|
|
999
1345
|
color: THEME.text,
|
|
1000
1346
|
fontSize: '14px',
|
|
1001
1347
|
resize: 'none',
|
|
1002
1348
|
outline: 'none',
|
|
1003
1349
|
fontFamily: 'inherit',
|
|
1004
|
-
minHeight: '
|
|
1350
|
+
minHeight: '24px',
|
|
1005
1351
|
maxHeight: '120px',
|
|
1006
1352
|
}}
|
|
1007
1353
|
rows={1}
|
|
1008
1354
|
/>
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1355
|
+
|
|
1356
|
+
{/* Bottom row with attach button and send */}
|
|
1357
|
+
<div style={{
|
|
1358
|
+
display: 'flex',
|
|
1359
|
+
alignItems: 'center',
|
|
1360
|
+
justifyContent: 'space-between',
|
|
1361
|
+
padding: '8px 12px',
|
|
1362
|
+
borderTop: `1px solid ${THEME.border}`,
|
|
1363
|
+
}}>
|
|
1364
|
+
{/* + Attach button */}
|
|
1365
|
+
<label
|
|
1366
|
+
style={{
|
|
1367
|
+
display: 'flex',
|
|
1368
|
+
alignItems: 'center',
|
|
1369
|
+
gap: '6px',
|
|
1370
|
+
padding: '6px 12px',
|
|
1371
|
+
background: THEME.bgHover,
|
|
1372
|
+
border: `1px solid ${THEME.borderSubtle}`,
|
|
1373
|
+
borderRadius: '6px',
|
|
1374
|
+
color: THEME.textMuted,
|
|
1375
|
+
fontSize: '13px',
|
|
1376
|
+
cursor: isGenerating ? 'not-allowed' : 'pointer',
|
|
1377
|
+
opacity: isGenerating ? 0.5 : 1,
|
|
1378
|
+
transition: 'all 0.2s',
|
|
1379
|
+
}}
|
|
1380
|
+
>
|
|
1381
|
+
<Icons.Plus />
|
|
1382
|
+
<span>Attach</span>
|
|
1383
|
+
<input
|
|
1384
|
+
type="file"
|
|
1385
|
+
accept="image/*"
|
|
1386
|
+
multiple
|
|
1387
|
+
style={{ display: 'none' }}
|
|
1388
|
+
disabled={isGenerating}
|
|
1389
|
+
onChange={(e) => {
|
|
1390
|
+
if (e.target.files) {
|
|
1391
|
+
Array.from(e.target.files).forEach(file => {
|
|
1392
|
+
if (file.type.startsWith('image/')) {
|
|
1393
|
+
const reader = new FileReader();
|
|
1394
|
+
reader.onload = (ev) => {
|
|
1395
|
+
const newImage: ImageAttachment = {
|
|
1396
|
+
id: generateId(),
|
|
1397
|
+
data: ev.target?.result as string,
|
|
1398
|
+
type: file.type,
|
|
1399
|
+
name: file.name,
|
|
1400
|
+
};
|
|
1401
|
+
setImages(prev => [...prev, newImage]);
|
|
1402
|
+
};
|
|
1403
|
+
reader.readAsDataURL(file);
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
}}
|
|
1408
|
+
/>
|
|
1409
|
+
</label>
|
|
1410
|
+
|
|
1411
|
+
{/* Send button */}
|
|
1412
|
+
<button
|
|
1413
|
+
onClick={sendMessage}
|
|
1414
|
+
disabled={isGenerating || !inputValue.trim()}
|
|
1415
|
+
style={{
|
|
1416
|
+
padding: '8px 12px',
|
|
1417
|
+
background: (isGenerating || !inputValue.trim()) ? THEME.bgHover : THEME.accent,
|
|
1418
|
+
border: 'none',
|
|
1419
|
+
borderRadius: '8px',
|
|
1420
|
+
color: (isGenerating || !inputValue.trim()) ? THEME.textSubtle : '#fff',
|
|
1421
|
+
cursor: (isGenerating || !inputValue.trim()) ? 'not-allowed' : 'pointer',
|
|
1422
|
+
display: 'flex',
|
|
1423
|
+
alignItems: 'center',
|
|
1424
|
+
justifyContent: 'center',
|
|
1425
|
+
}}
|
|
1426
|
+
>
|
|
1427
|
+
<Icons.Send />
|
|
1428
|
+
</button>
|
|
1429
|
+
</div>
|
|
1025
1430
|
</div>
|
|
1026
1431
|
</div>
|
|
1027
1432
|
|