@tpitre/story-ui 2.3.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/production-app/src/App.tsx +472 -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
|
};
|
|
@@ -548,6 +787,7 @@ OUTPUT: Start immediately with < and output only JSX.`;
|
|
|
548
787
|
messages: conversationHistory,
|
|
549
788
|
systemPrompt,
|
|
550
789
|
prefillAssistant,
|
|
790
|
+
model: serverConfig.currentModel,
|
|
551
791
|
maxTokens: 4096,
|
|
552
792
|
images: imageAttachments.map(img => ({
|
|
553
793
|
type: img.type,
|
|
@@ -588,7 +828,7 @@ OUTPUT: Start immediately with < and output only JSX.`;
|
|
|
588
828
|
};
|
|
589
829
|
|
|
590
830
|
const sendMessage = async () => {
|
|
591
|
-
if (!inputValue.trim() ||
|
|
831
|
+
if (!inputValue.trim() || isGenerating) return;
|
|
592
832
|
|
|
593
833
|
const userMessage: Message = {
|
|
594
834
|
id: generateId(),
|
|
@@ -598,17 +838,26 @@ OUTPUT: Start immediately with < and output only JSX.`;
|
|
|
598
838
|
images: images.length > 0 ? [...images] : undefined,
|
|
599
839
|
};
|
|
600
840
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
841
|
+
// Determine if we need to create a new conversation or add to existing
|
|
842
|
+
let conversationId = activeConversationId;
|
|
843
|
+
|
|
844
|
+
if (!conversationId) {
|
|
845
|
+
// Create new conversation with the first message
|
|
846
|
+
conversationId = createConversationWithMessage(userMessage);
|
|
847
|
+
setActiveConversationId(conversationId);
|
|
848
|
+
} else {
|
|
849
|
+
// Add message to existing conversation
|
|
850
|
+
setConversations(prev => prev.map(conv => {
|
|
851
|
+
if (conv.id === conversationId) {
|
|
852
|
+
return {
|
|
853
|
+
...conv,
|
|
854
|
+
messages: [...conv.messages, userMessage],
|
|
855
|
+
updatedAt: Date.now(),
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
return conv;
|
|
859
|
+
}));
|
|
860
|
+
}
|
|
612
861
|
|
|
613
862
|
const currentImages = [...images];
|
|
614
863
|
setInputValue('');
|
|
@@ -627,7 +876,7 @@ OUTPUT: Start immediately with < and output only JSX.`;
|
|
|
627
876
|
};
|
|
628
877
|
|
|
629
878
|
setConversations(prev => prev.map(conv => {
|
|
630
|
-
if (conv.id ===
|
|
879
|
+
if (conv.id === conversationId) {
|
|
631
880
|
return {
|
|
632
881
|
...conv,
|
|
633
882
|
messages: [...conv.messages, assistantMessage],
|
|
@@ -650,7 +899,7 @@ OUTPUT: Start immediately with < and output only JSX.`;
|
|
|
650
899
|
};
|
|
651
900
|
|
|
652
901
|
setConversations(prev => prev.map(conv => {
|
|
653
|
-
if (conv.id ===
|
|
902
|
+
if (conv.id === conversationId) {
|
|
654
903
|
return {
|
|
655
904
|
...conv,
|
|
656
905
|
messages: [...conv.messages, errorMessage],
|
|
@@ -740,7 +989,7 @@ OUTPUT: Start immediately with < and output only JSX.`;
|
|
|
740
989
|
{/* New Chat Button */}
|
|
741
990
|
<div style={{ padding: sidebarCollapsed ? '8px' : '12px' }}>
|
|
742
991
|
<button
|
|
743
|
-
onClick={
|
|
992
|
+
onClick={startNewChat}
|
|
744
993
|
style={{
|
|
745
994
|
width: '100%',
|
|
746
995
|
padding: sidebarCollapsed ? '10px' : '10px 14px',
|
|
@@ -765,7 +1014,7 @@ OUTPUT: Start immediately with < and output only JSX.`;
|
|
|
765
1014
|
{/* Conversation List */}
|
|
766
1015
|
{!sidebarCollapsed && (
|
|
767
1016
|
<div style={{ flex: 1, overflow: 'auto', padding: '8px 12px' }}>
|
|
768
|
-
{
|
|
1017
|
+
{displayConversations.map(conv => (
|
|
769
1018
|
<div
|
|
770
1019
|
key={conv.id}
|
|
771
1020
|
onClick={() => {
|
|
@@ -815,15 +1064,62 @@ OUTPUT: Start immediately with < and output only JSX.`;
|
|
|
815
1064
|
</div>
|
|
816
1065
|
)}
|
|
817
1066
|
|
|
818
|
-
{/*
|
|
1067
|
+
{/* Sidebar Footer - Provider/Model Info */}
|
|
819
1068
|
{!sidebarCollapsed && (
|
|
820
1069
|
<div style={{
|
|
821
1070
|
padding: '12px',
|
|
822
1071
|
borderTop: `1px solid ${THEME.border}`,
|
|
823
|
-
|
|
824
|
-
|
|
1072
|
+
display: 'flex',
|
|
1073
|
+
flexDirection: 'column',
|
|
1074
|
+
gap: '6px',
|
|
825
1075
|
}}>
|
|
826
|
-
|
|
1076
|
+
{/* Provider Label */}
|
|
1077
|
+
<div style={{ fontSize: '12px', color: THEME.textMuted }}>
|
|
1078
|
+
<span style={{ color: THEME.textSubtle, textTransform: 'uppercase', fontSize: '10px', letterSpacing: '0.5px' }}>Provider: </span>
|
|
1079
|
+
<span style={{ color: THEME.text, fontWeight: 500 }}>
|
|
1080
|
+
{serverConfig.providers.find(p => p.id === serverConfig.currentProvider)?.name || 'Claude (Anthropic)'}
|
|
1081
|
+
</span>
|
|
1082
|
+
</div>
|
|
1083
|
+
|
|
1084
|
+
{/* Model Dropdown */}
|
|
1085
|
+
<div style={{ fontSize: '12px', color: THEME.textMuted }}>
|
|
1086
|
+
<span style={{ color: THEME.textSubtle, textTransform: 'uppercase', fontSize: '10px', letterSpacing: '0.5px' }}>Model</span>
|
|
1087
|
+
</div>
|
|
1088
|
+
<select
|
|
1089
|
+
value={serverConfig.currentModel}
|
|
1090
|
+
onChange={(e) => serverConfig.changeModel(e.target.value)}
|
|
1091
|
+
disabled={serverConfig.loading}
|
|
1092
|
+
style={{
|
|
1093
|
+
width: '100%',
|
|
1094
|
+
padding: '8px 10px',
|
|
1095
|
+
background: THEME.bgElevated,
|
|
1096
|
+
border: `1px solid ${THEME.border}`,
|
|
1097
|
+
borderRadius: '6px',
|
|
1098
|
+
color: THEME.text,
|
|
1099
|
+
fontSize: '12px',
|
|
1100
|
+
cursor: 'pointer',
|
|
1101
|
+
outline: 'none',
|
|
1102
|
+
appearance: 'none',
|
|
1103
|
+
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")`,
|
|
1104
|
+
backgroundRepeat: 'no-repeat',
|
|
1105
|
+
backgroundPosition: 'right 8px center',
|
|
1106
|
+
paddingRight: '28px',
|
|
1107
|
+
}}
|
|
1108
|
+
>
|
|
1109
|
+
{serverConfig.models.map(model => (
|
|
1110
|
+
<option key={model.id} value={model.id}>{model.id}</option>
|
|
1111
|
+
))}
|
|
1112
|
+
</select>
|
|
1113
|
+
|
|
1114
|
+
{/* Components Count */}
|
|
1115
|
+
<div style={{
|
|
1116
|
+
display: 'flex',
|
|
1117
|
+
alignItems: 'center',
|
|
1118
|
+
gap: '6px',
|
|
1119
|
+
fontSize: '11px',
|
|
1120
|
+
color: THEME.textSubtle,
|
|
1121
|
+
paddingTop: '4px',
|
|
1122
|
+
}}>
|
|
827
1123
|
<div style={{
|
|
828
1124
|
width: '6px',
|
|
829
1125
|
height: '6px',
|
|
@@ -847,7 +1143,7 @@ OUTPUT: Start immediately with < and output only JSX.`;
|
|
|
847
1143
|
}}>
|
|
848
1144
|
{/* Messages */}
|
|
849
1145
|
<div style={{ flex: 1, overflow: 'auto', padding: '16px' }}>
|
|
850
|
-
{activeConversation
|
|
1146
|
+
{(!activeConversation || activeConversation.messages.length === 0) && (
|
|
851
1147
|
<div style={{
|
|
852
1148
|
padding: '32px 16px',
|
|
853
1149
|
textAlign: 'center',
|
|
@@ -970,19 +1266,68 @@ OUTPUT: Start immediately with < and output only JSX.`;
|
|
|
970
1266
|
<div ref={messagesEndRef} />
|
|
971
1267
|
</div>
|
|
972
1268
|
|
|
973
|
-
{/* Input Area */}
|
|
1269
|
+
{/* Input Area - ChatGPT/Lovable Style */}
|
|
974
1270
|
<div style={{
|
|
975
1271
|
padding: '16px',
|
|
976
1272
|
borderTop: `1px solid ${THEME.border}`,
|
|
977
1273
|
background: THEME.bgSurface,
|
|
978
1274
|
}}>
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1275
|
+
{/* Image thumbnails if any */}
|
|
1276
|
+
{images.length > 0 && (
|
|
1277
|
+
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginBottom: '12px' }}>
|
|
1278
|
+
{images.map(img => (
|
|
1279
|
+
<div
|
|
1280
|
+
key={img.id}
|
|
1281
|
+
style={{
|
|
1282
|
+
position: 'relative',
|
|
1283
|
+
width: '64px',
|
|
1284
|
+
height: '64px',
|
|
1285
|
+
borderRadius: '8px',
|
|
1286
|
+
overflow: 'hidden',
|
|
1287
|
+
border: `1px solid ${THEME.border}`,
|
|
1288
|
+
}}
|
|
1289
|
+
>
|
|
1290
|
+
<img
|
|
1291
|
+
src={img.data}
|
|
1292
|
+
alt={img.name}
|
|
1293
|
+
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
|
1294
|
+
/>
|
|
1295
|
+
<button
|
|
1296
|
+
onClick={() => setImages(images.filter(i => i.id !== img.id))}
|
|
1297
|
+
style={{
|
|
1298
|
+
position: 'absolute',
|
|
1299
|
+
top: '2px',
|
|
1300
|
+
right: '2px',
|
|
1301
|
+
width: '18px',
|
|
1302
|
+
height: '18px',
|
|
1303
|
+
borderRadius: '50%',
|
|
1304
|
+
background: 'rgba(0,0,0,0.7)',
|
|
1305
|
+
border: 'none',
|
|
1306
|
+
color: '#fff',
|
|
1307
|
+
cursor: 'pointer',
|
|
1308
|
+
display: 'flex',
|
|
1309
|
+
alignItems: 'center',
|
|
1310
|
+
justifyContent: 'center',
|
|
1311
|
+
fontSize: '10px',
|
|
1312
|
+
}}
|
|
1313
|
+
>
|
|
1314
|
+
<Icons.X />
|
|
1315
|
+
</button>
|
|
1316
|
+
</div>
|
|
1317
|
+
))}
|
|
1318
|
+
</div>
|
|
1319
|
+
)}
|
|
984
1320
|
|
|
985
|
-
|
|
1321
|
+
{/* Combined input container matching reference */}
|
|
1322
|
+
<div style={{
|
|
1323
|
+
display: 'flex',
|
|
1324
|
+
flexDirection: 'column',
|
|
1325
|
+
background: THEME.bgElevated,
|
|
1326
|
+
border: `1px solid ${THEME.border}`,
|
|
1327
|
+
borderRadius: '12px',
|
|
1328
|
+
overflow: 'hidden',
|
|
1329
|
+
}}>
|
|
1330
|
+
{/* Text input area */}
|
|
986
1331
|
<textarea
|
|
987
1332
|
ref={inputRef}
|
|
988
1333
|
value={inputValue}
|
|
@@ -991,37 +1336,95 @@ OUTPUT: Start immediately with < and output only JSX.`;
|
|
|
991
1336
|
placeholder="Describe the component you want to create..."
|
|
992
1337
|
disabled={isGenerating}
|
|
993
1338
|
style={{
|
|
994
|
-
|
|
995
|
-
padding: '
|
|
996
|
-
background:
|
|
997
|
-
border:
|
|
998
|
-
borderRadius: '10px',
|
|
1339
|
+
width: '100%',
|
|
1340
|
+
padding: '14px 16px',
|
|
1341
|
+
background: 'transparent',
|
|
1342
|
+
border: 'none',
|
|
999
1343
|
color: THEME.text,
|
|
1000
1344
|
fontSize: '14px',
|
|
1001
1345
|
resize: 'none',
|
|
1002
1346
|
outline: 'none',
|
|
1003
1347
|
fontFamily: 'inherit',
|
|
1004
|
-
minHeight: '
|
|
1348
|
+
minHeight: '24px',
|
|
1005
1349
|
maxHeight: '120px',
|
|
1006
1350
|
}}
|
|
1007
1351
|
rows={1}
|
|
1008
1352
|
/>
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1353
|
+
|
|
1354
|
+
{/* Bottom row with attach button and send */}
|
|
1355
|
+
<div style={{
|
|
1356
|
+
display: 'flex',
|
|
1357
|
+
alignItems: 'center',
|
|
1358
|
+
justifyContent: 'space-between',
|
|
1359
|
+
padding: '8px 12px',
|
|
1360
|
+
borderTop: `1px solid ${THEME.border}`,
|
|
1361
|
+
}}>
|
|
1362
|
+
{/* + Attach button */}
|
|
1363
|
+
<label
|
|
1364
|
+
style={{
|
|
1365
|
+
display: 'flex',
|
|
1366
|
+
alignItems: 'center',
|
|
1367
|
+
gap: '6px',
|
|
1368
|
+
padding: '6px 12px',
|
|
1369
|
+
background: THEME.bgHover,
|
|
1370
|
+
border: `1px solid ${THEME.borderSubtle}`,
|
|
1371
|
+
borderRadius: '6px',
|
|
1372
|
+
color: THEME.textMuted,
|
|
1373
|
+
fontSize: '13px',
|
|
1374
|
+
cursor: isGenerating ? 'not-allowed' : 'pointer',
|
|
1375
|
+
opacity: isGenerating ? 0.5 : 1,
|
|
1376
|
+
transition: 'all 0.2s',
|
|
1377
|
+
}}
|
|
1378
|
+
>
|
|
1379
|
+
<Icons.Plus />
|
|
1380
|
+
<span>Attach</span>
|
|
1381
|
+
<input
|
|
1382
|
+
type="file"
|
|
1383
|
+
accept="image/*"
|
|
1384
|
+
multiple
|
|
1385
|
+
style={{ display: 'none' }}
|
|
1386
|
+
disabled={isGenerating}
|
|
1387
|
+
onChange={(e) => {
|
|
1388
|
+
if (e.target.files) {
|
|
1389
|
+
Array.from(e.target.files).forEach(file => {
|
|
1390
|
+
if (file.type.startsWith('image/')) {
|
|
1391
|
+
const reader = new FileReader();
|
|
1392
|
+
reader.onload = (ev) => {
|
|
1393
|
+
const newImage: ImageAttachment = {
|
|
1394
|
+
id: generateId(),
|
|
1395
|
+
data: ev.target?.result as string,
|
|
1396
|
+
type: file.type,
|
|
1397
|
+
name: file.name,
|
|
1398
|
+
};
|
|
1399
|
+
setImages(prev => [...prev, newImage]);
|
|
1400
|
+
};
|
|
1401
|
+
reader.readAsDataURL(file);
|
|
1402
|
+
}
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
}}
|
|
1406
|
+
/>
|
|
1407
|
+
</label>
|
|
1408
|
+
|
|
1409
|
+
{/* Send button */}
|
|
1410
|
+
<button
|
|
1411
|
+
onClick={sendMessage}
|
|
1412
|
+
disabled={isGenerating || !inputValue.trim()}
|
|
1413
|
+
style={{
|
|
1414
|
+
padding: '8px 12px',
|
|
1415
|
+
background: (isGenerating || !inputValue.trim()) ? THEME.bgHover : THEME.accent,
|
|
1416
|
+
border: 'none',
|
|
1417
|
+
borderRadius: '8px',
|
|
1418
|
+
color: (isGenerating || !inputValue.trim()) ? THEME.textSubtle : '#fff',
|
|
1419
|
+
cursor: (isGenerating || !inputValue.trim()) ? 'not-allowed' : 'pointer',
|
|
1420
|
+
display: 'flex',
|
|
1421
|
+
alignItems: 'center',
|
|
1422
|
+
justifyContent: 'center',
|
|
1423
|
+
}}
|
|
1424
|
+
>
|
|
1425
|
+
<Icons.Send />
|
|
1426
|
+
</button>
|
|
1427
|
+
</div>
|
|
1025
1428
|
</div>
|
|
1026
1429
|
</div>
|
|
1027
1430
|
|