@messenger-box/tailwind-ui-inbox 10.0.3-alpha.74 → 10.0.3-alpha.78

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.
Files changed (29) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/lib/components/AIAgent/AIAgent.d.ts.map +1 -1
  3. package/lib/components/AIAgent/AIAgent.js +13 -4
  4. package/lib/components/AIAgent/AIAgent.js.map +1 -1
  5. package/lib/components/InboxMessage/InputComponent.d.ts +4 -1
  6. package/lib/components/InboxMessage/InputComponent.d.ts.map +1 -1
  7. package/lib/components/InboxMessage/InputComponent.js +172 -26
  8. package/lib/components/InboxMessage/InputComponent.js.map +1 -1
  9. package/lib/components/InboxMessage/message-widgets/ModernMessageGroup.d.ts.map +1 -1
  10. package/lib/components/InboxMessage/message-widgets/ModernMessageGroup.js +137 -31
  11. package/lib/components/InboxMessage/message-widgets/ModernMessageGroup.js.map +1 -1
  12. package/lib/components/ModelConfigPanel.d.ts +25 -0
  13. package/lib/components/ModelConfigPanel.d.ts.map +1 -1
  14. package/lib/components/ModelConfigPanel.js +14 -1
  15. package/lib/components/ModelConfigPanel.js.map +1 -1
  16. package/lib/container/AiLandingInput.d.ts.map +1 -1
  17. package/lib/container/AiLandingInput.js +9 -15
  18. package/lib/container/AiLandingInput.js.map +1 -1
  19. package/lib/container/Inbox.js +1 -1
  20. package/lib/container/ServiceInbox.js +1 -1
  21. package/lib/container/ThreadMessages.js +1 -1
  22. package/lib/container/ThreadMessagesInbox.js +1 -1
  23. package/lib/container/Threads.js +1 -1
  24. package/package.json +4 -4
  25. package/src/components/AIAgent/AIAgent.tsx +19 -5
  26. package/src/components/InboxMessage/InputComponent.tsx +241 -43
  27. package/src/components/InboxMessage/message-widgets/ModernMessageGroup.tsx +152 -46
  28. package/src/components/ModelConfigPanel.tsx +13 -2
  29. package/src/container/AiLandingInput.tsx +20 -9
@@ -3,21 +3,35 @@ import { useTranslation } from 'react-i18next';
3
3
  import { config } from '../../config';
4
4
  import { UploadImageButton } from './UploadImageButton';
5
5
  import { FilesList } from '../inbox';
6
+ import { ModelConfigPanel, modelOptions, templateOptions, getAllModels } from '../ModelConfigPanel';
7
+ import { ModelConfig } from '../../hooks/usePersistentModelConfig';
6
8
 
7
9
  type MessageInputProps = {
8
10
  channelId?: string;
9
11
  handleSend?: (message: string, files: File[]) => Promise<void>;
10
12
  placeholder?: string;
13
+ modelConfig?: ModelConfig;
14
+ onModelConfigChange?: (config: ModelConfig) => void;
11
15
  };
12
16
 
13
- export const InputComponent = ({ handleSend: handleSendProp, placeholder }: MessageInputProps) => {
17
+ export const InputComponent = ({
18
+ handleSend: handleSendProp,
19
+ placeholder,
20
+ modelConfig,
21
+ onModelConfigChange,
22
+ }: MessageInputProps) => {
14
23
  const [message, setMessage] = useState('');
15
24
  const [sending, setSending] = useState(false);
16
25
  const [files, setFiles] = useState<File[]>([]);
17
26
  const [showToast, setShowToast] = useState(false);
18
27
  const [toastMessage, setToastMessage] = useState('');
19
28
  const [isFocused, setIsFocused] = useState(false);
29
+ const [showModelDropdown, setShowModelDropdown] = useState(false);
30
+ const [showTemplateDropdown, setShowTemplateDropdown] = useState(false);
31
+ const [showSettingsModal, setShowSettingsModal] = useState(false);
20
32
  const textareaRef = useRef<HTMLTextAreaElement>(null);
33
+ const modelDropdownRef = useRef<HTMLDivElement>(null);
34
+ const templateDropdownRef = useRef<HTMLDivElement>(null);
21
35
  const { t } = useTranslation('translations');
22
36
 
23
37
  // Auto-focus the textarea when component mounts
@@ -27,6 +41,20 @@ export const InputComponent = ({ handleSend: handleSendProp, placeholder }: Mess
27
41
  }
28
42
  }, []);
29
43
 
44
+ // Handle click outside for dropdowns
45
+ useEffect(() => {
46
+ const handleClickOutside = (event: MouseEvent) => {
47
+ if (modelDropdownRef.current && !modelDropdownRef.current.contains(event.target as Node)) {
48
+ setShowModelDropdown(false);
49
+ }
50
+ if (templateDropdownRef.current && !templateDropdownRef.current.contains(event.target as Node)) {
51
+ setShowTemplateDropdown(false);
52
+ }
53
+ };
54
+ document.addEventListener('mousedown', handleClickOutside);
55
+ return () => document.removeEventListener('mousedown', handleClickOutside);
56
+ }, []);
57
+
30
58
  const showToastMessage = useCallback((message: string) => {
31
59
  setToastMessage(message);
32
60
  setShowToast(true);
@@ -91,6 +119,35 @@ export const InputComponent = ({ handleSend: handleSendProp, placeholder }: Mess
91
119
  const canSend = message.trim() || files.length > 0;
92
120
  const hasContent = message.trim().length > 0;
93
121
 
122
+ // Get models and templates from ModelConfigPanel
123
+ const allModelOptions = useMemo(() => {
124
+ return getAllModels();
125
+ }, []);
126
+
127
+ const templateOptionsList = useMemo(() => {
128
+ return templateOptions.map((t) => ({ value: t.value, label: t.label, icon: t.icon }));
129
+ }, []);
130
+
131
+ const handleModelSelect = useCallback(
132
+ (modelValue: string) => {
133
+ if (onModelConfigChange && modelConfig) {
134
+ onModelConfigChange({ ...modelConfig, model: modelValue });
135
+ }
136
+ setShowModelDropdown(false);
137
+ },
138
+ [modelConfig, onModelConfigChange],
139
+ );
140
+
141
+ const handleTemplateSelect = useCallback(
142
+ (templateValue: string) => {
143
+ if (onModelConfigChange && modelConfig) {
144
+ onModelConfigChange({ ...modelConfig, template: templateValue as any });
145
+ }
146
+ setShowTemplateDropdown(false);
147
+ },
148
+ [modelConfig, onModelConfigChange],
149
+ );
150
+
94
151
  return (
95
152
  // <div className="bg-gray-50 border-t border-gray-200">
96
153
  <div className="bg-gray-50 border border-gray-200">
@@ -142,7 +199,7 @@ export const InputComponent = ({ handleSend: handleSendProp, placeholder }: Mess
142
199
  />
143
200
  </div>
144
201
 
145
- {/* Toolbar buttons row at bottom */}
202
+ {/* Simplified Toolbar - Only Template, Model, Upload, Send, and Settings */}
146
203
  <div className="flex items-center gap-2">
147
204
  <UploadImageButton onChange={onUploadImageChange}>
148
205
  <div className="flex items-center justify-center w-8 h-8 rounded-lg border border-gray-300 bg-white hover:bg-gray-50 transition-colors">
@@ -156,43 +213,123 @@ export const InputComponent = ({ handleSend: handleSendProp, placeholder }: Mess
156
213
  </svg>
157
214
  </div>
158
215
  </UploadImageButton>
216
+ {/* 1. Template Selection Dropdown */}
217
+ <div className="relative" ref={templateDropdownRef}>
218
+ <button
219
+ onClick={() => setShowTemplateDropdown(!showTemplateDropdown)}
220
+ className="flex items-center gap-2 px-3 py-1.5 text-sm rounded-lg border border-gray-300 bg-white hover:bg-gray-50 transition-colors min-w-[120px]"
221
+ >
222
+ {(() => {
223
+ if (modelConfig && modelConfig.template) {
224
+ const currentTemplate = templateOptionsList.find(
225
+ (option) => option.value === modelConfig.template,
226
+ );
227
+ return currentTemplate ? (
228
+ <>
229
+ <span className="text-lg">{currentTemplate.icon}</span>
230
+ <span className="text-gray-700">{currentTemplate.label}</span>
231
+ </>
232
+ ) : (
233
+ <>
234
+ <span className="text-lg">📝</span>
235
+ <span className="text-gray-500">Template</span>
236
+ </>
237
+ );
238
+ }
239
+ return (
240
+ <>
241
+ <span className="text-lg">📝</span>
242
+ <span className="text-gray-500">Template</span>
243
+ </>
244
+ );
245
+ })()}
246
+ <svg
247
+ className="w-4 h-4 text-gray-500 ml-auto"
248
+ fill="none"
249
+ stroke="currentColor"
250
+ viewBox="0 0 24 24"
251
+ >
252
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
253
+ </svg>
254
+ </button>
159
255
 
160
- <button className="flex items-center justify-center w-8 h-8 rounded-lg border border-gray-300 bg-white hover:bg-gray-50 transition-colors">
161
- <svg className="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
162
- <path
163
- strokeLinecap="round"
164
- strokeLinejoin="round"
165
- strokeWidth={2}
166
- d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
167
- />
168
- </svg>
169
- </button>
256
+ {showTemplateDropdown && (
257
+ <div className="absolute bottom-full left-0 mb-2 w-48 bg-white border border-gray-200 rounded-lg shadow-lg z-50">
258
+ <div className="py-1">
259
+ {templateOptionsList.map((option) => (
260
+ <button
261
+ key={option.value}
262
+ onClick={() => handleTemplateSelect(option.value)}
263
+ className={`w-full px-3 py-2 text-left hover:bg-gray-50 transition-colors flex items-center gap-2 ${
264
+ modelConfig?.template === option.value ? 'bg-blue-50' : ''
265
+ }`}
266
+ >
267
+ <span className="text-lg">{option.icon}</span>
268
+ <span className="font-medium text-gray-900">{option.label}</span>
269
+ </button>
270
+ ))}
271
+ </div>
272
+ </div>
273
+ )}
274
+ </div>
170
275
 
171
- <button className="flex items-center justify-center w-8 h-8 rounded-lg border border-gray-300 bg-white hover:bg-gray-50 transition-colors">
172
- <svg className="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
173
- <path
174
- strokeLinecap="round"
175
- strokeLinejoin="round"
176
- strokeWidth={2}
177
- d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
178
- />
179
- </svg>
180
- </button>
276
+ {/* 2. Model Selection Dropdown */}
277
+ <div className="relative" ref={modelDropdownRef}>
278
+ <button
279
+ onClick={() => setShowModelDropdown(!showModelDropdown)}
280
+ className="flex items-center gap-2 px-3 py-1.5 text-sm rounded-lg border border-gray-300 bg-white hover:bg-gray-50 transition-colors min-w-[140px]"
281
+ >
282
+ {(() => {
283
+ if (modelConfig && modelConfig.model) {
284
+ const currentModel = allModelOptions.find(
285
+ (option) => option.value === modelConfig.model,
286
+ );
287
+ return currentModel ? (
288
+ <span className="text-gray-700">{currentModel.label}</span>
289
+ ) : (
290
+ <span className="text-gray-500">Model</span>
291
+ );
292
+ }
293
+ return <span className="text-gray-500">Model</span>;
294
+ })()}
295
+ <svg
296
+ className="w-4 h-4 text-gray-500 ml-auto"
297
+ fill="none"
298
+ stroke="currentColor"
299
+ viewBox="0 0 24 24"
300
+ >
301
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
302
+ </svg>
303
+ </button>
181
304
 
182
- <button className="flex items-center justify-center w-8 h-8 rounded-lg border border-gray-300 bg-white hover:bg-gray-50 transition-colors">
183
- <svg className="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
184
- <path
185
- strokeLinecap="round"
186
- strokeLinejoin="round"
187
- strokeWidth={2}
188
- d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
189
- />
190
- </svg>
191
- </button>
305
+ {showModelDropdown && (
306
+ <div className="absolute bottom-full left-0 mb-2 w-64 bg-white border border-gray-200 rounded-lg shadow-lg z-50 max-h-60 overflow-y-auto">
307
+ <div className="py-1">
308
+ {allModelOptions.map((option) => (
309
+ <button
310
+ key={option.value}
311
+ onClick={() => handleModelSelect(option.value)}
312
+ className={`w-full px-3 py-2 text-left hover:bg-gray-50 transition-colors ${
313
+ modelConfig?.model === option.value ? 'bg-blue-50' : ''
314
+ }`}
315
+ >
316
+ <div className="font-medium text-gray-900">{option.label}</div>
317
+ </button>
318
+ ))}
319
+ </div>
320
+ </div>
321
+ )}
322
+ </div>
192
323
 
193
324
  <div className="flex-1"></div>
194
325
 
195
- <button className="flex items-center justify-center w-8 h-8 rounded-lg border border-gray-300 bg-white hover:bg-gray-50 transition-colors">
326
+ {/* 3. Upload Button */}
327
+
328
+ {/* 4. Project Settings Button */}
329
+ <button
330
+ onClick={() => setShowSettingsModal(true)}
331
+ className="flex items-center justify-center w-8 h-8 rounded-lg border border-gray-300 bg-white hover:bg-gray-50 transition-colors"
332
+ >
196
333
  <svg className="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
197
334
  <path
198
335
  strokeLinecap="round"
@@ -209,7 +346,7 @@ export const InputComponent = ({ handleSend: handleSendProp, placeholder }: Mess
209
346
  </svg>
210
347
  </button>
211
348
 
212
- {/* Send button - updated to handle sending messages */}
349
+ {/* 5. Send Button */}
213
350
  <button
214
351
  className={`flex items-center justify-center w-8 h-8 rounded-lg border transition-colors ${
215
352
  canSend && !sending
@@ -250,15 +387,76 @@ export const InputComponent = ({ handleSend: handleSendProp, placeholder }: Mess
250
387
  </div>
251
388
  </div>
252
389
 
253
- {/* Beta text and notification icon */}
254
- {/* <div className="px-4 py-2 bg-gray-50 border-t border-gray-200 flex items-center justify-between">
255
- <span className="text-sm text-gray-500">This feature is in beta. Send feedback</span>
256
- <button className="p-1 hover:bg-gray-100 rounded transition-colors">
257
- <svg className="w-5 h-5 text-gray-500" fill="currentColor" viewBox="0 0 24 24">
258
- <path d="M12 2C13.1 2 14 2.9 14 4C14 5.1 13.1 6 12 6C10.9 6 10 5.1 10 4C10 2.9 10.9 2 12 2ZM21 19V20H3V19L5 17V11C5 7.9 7.03 5.17 10 4.29C10 4.19 10 4.1 10 4C10 2.9 10.9 2 12 2C13.1 2 14 2.9 14 4C14 4.1 14 4.19 14 4.29C16.97 5.17 19 7.9 19 11V17L21 19ZM14 21C14 22.1 13.1 23 12 23C10.9 23 10 22.1 10 21" />
259
- </svg>
260
- </button>
261
- </div> */}
390
+ {/* Simplified Settings Modal - Only API Key and Model */}
391
+ {showSettingsModal && (
392
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
393
+ <div className="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
394
+ <div className="p-6 mb-4">
395
+ <div className="flex items-center justify-between mb-4">
396
+ <h3 className="text-lg font-semibold text-gray-900">Project Settings</h3>
397
+ <button
398
+ onClick={() => setShowSettingsModal(false)}
399
+ className="text-gray-400 hover:text-gray-600 transition-colors"
400
+ >
401
+ <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
402
+ <path
403
+ strokeLinecap="round"
404
+ strokeLinejoin="round"
405
+ strokeWidth={2}
406
+ d="M6 18L18 6M6 6l12 12"
407
+ />
408
+ </svg>
409
+ </button>
410
+ </div>
411
+
412
+ {modelConfig && onModelConfigChange && (
413
+ <div className="space-y-4">
414
+ {/* API Key Input */}
415
+ <div>
416
+ <label className="block text-sm font-medium text-gray-700 mb-2">API Key</label>
417
+ <input
418
+ type="password"
419
+ value={modelConfig.apiKey}
420
+ onChange={(e) =>
421
+ onModelConfigChange({ ...modelConfig, apiKey: e.target.value })
422
+ }
423
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
424
+ placeholder="Enter your API key"
425
+ />
426
+ </div>
427
+
428
+ {/* Model Selection */}
429
+ <div>
430
+ <label className="block text-sm font-medium text-gray-700 mb-2">Model</label>
431
+ <select
432
+ value={modelConfig.model}
433
+ onChange={(e) =>
434
+ onModelConfigChange({ ...modelConfig, model: e.target.value })
435
+ }
436
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
437
+ >
438
+ {allModelOptions.map((option) => (
439
+ <option key={option.value} value={option.value}>
440
+ {option.label}
441
+ </option>
442
+ ))}
443
+ </select>
444
+ </div>
445
+ </div>
446
+ )}
447
+
448
+ {/* <div className="mt-6 flex justify-end">
449
+ <button
450
+ onClick={() => setShowSettingsModal(false)}
451
+ className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
452
+ >
453
+ Save Settings
454
+ </button>
455
+ </div> */}
456
+ </div>
457
+ </div>
458
+ </div>
459
+ )}
262
460
  </div>
263
461
  );
264
462
  };
@@ -183,7 +183,8 @@ const htmlContentStyles = `
183
183
  /* Enhanced message container styling */
184
184
  .message-container {
185
185
  background: #ffffff;
186
- padding: 16px;
186
+ padding-top: 16px;
187
+ padding-bottom: 16px;
187
188
  margin: 12px 0;
188
189
  transition: all 0.2s ease-in-out;
189
190
  }
@@ -456,28 +457,59 @@ export const groupMessagesByUserAndTime = (messages: IPost[], timeThresholdMinut
456
457
  return groups;
457
458
  };
458
459
 
459
- // Enhanced HTML detection that recognizes more HTML patterns
460
- const isProbablyHTML = (value: string): boolean => {
461
- if (!value) return false;
462
-
463
- // Check for HTML tags (including self-closing tags)
464
- const htmlTagPattern = /<\/?[a-z][\s\S]*>/i;
465
-
466
- // Check for HTML entities
467
- const htmlEntityPattern = /&[a-z0-9#]+;/i;
468
-
469
- // Check for common HTML attributes
470
- const htmlAttrPattern = /\s+[a-z-]+\s*=\s*["'][^"']*["']/i;
471
-
472
- // Check for HTML-like structure
473
- const htmlStructurePattern = /<[^>]+>[^<]*<\/[^>]+>/i;
460
+ // Enhanced content detection that properly identifies different content types
461
+ const detectContentType = (value: string): 'html' | 'markdown' | 'code' | 'text' => {
462
+ if (!value) return 'text';
463
+
464
+ const trimmed = value.trim();
465
+
466
+ // Check for code patterns first (most specific)
467
+ const codePatterns = [
468
+ /```[\s\S]*?```/g, // Fenced code blocks
469
+ /`[^`\n]+`/g, // Inline code
470
+ /---\s*\w+\.\w+\s*---/g, // File separators like "--- src/index.css ---"
471
+ /@tailwind\s+\w+/g, // Tailwind directives
472
+ /export\s+default\s*\{/g, // JS/TS exports
473
+ /import\s+.*\s+from\s+['"]/g, // Import statements
474
+ /\/\*\*[\s\S]*?\*\//g, // JSDoc comments
475
+ /\/\*[\s\S]*?\*\//g, // Block comments
476
+ /\/\/.*$/gm, // Line comments
477
+ ];
478
+
479
+ const hasCodePatterns = codePatterns.some((pattern) => pattern.test(trimmed));
480
+ if (hasCodePatterns) return 'code';
481
+
482
+ // Check for markdown patterns
483
+ const markdownPatterns = [
484
+ /^#{1,6}\s+/gm, // Headers
485
+ /\*\*.*?\*\*/g, // Bold
486
+ /\*.*?\*/g, // Italic
487
+ /^\s*[-*+]\s+/gm, // Lists
488
+ /^\s*\d+\.\s+/gm, // Numbered lists
489
+ /\[.*?\]\(.*?\)/g, // Links
490
+ /!\[.*?\]\(.*?\)/g, // Images
491
+ ];
492
+
493
+ const hasMarkdownPatterns = markdownPatterns.some((pattern) => pattern.test(trimmed));
494
+ if (hasMarkdownPatterns) return 'markdown';
495
+
496
+ // Check for HTML patterns
497
+ const htmlPatterns = [
498
+ /<\/?[a-z][\s\S]*>/i, // HTML tags
499
+ /&[a-z0-9#]+;/i, // HTML entities
500
+ /\s+[a-z-]+\s*=\s*["'][^"']*["']/i, // HTML attributes
501
+ /<[^>]+>[^<]*<\/[^>]+>/i, // HTML structure
502
+ ];
503
+
504
+ const hasHtmlPatterns = htmlPatterns.some((pattern) => pattern.test(trimmed));
505
+ if (hasHtmlPatterns) return 'html';
506
+
507
+ return 'text';
508
+ };
474
509
 
475
- return (
476
- htmlTagPattern.test(value.trim()) ||
477
- htmlEntityPattern.test(value.trim()) ||
478
- htmlAttrPattern.test(value.trim()) ||
479
- htmlStructurePattern.test(value.trim())
480
- );
510
+ // Legacy function for backward compatibility
511
+ const isProbablyHTML = (value: string): boolean => {
512
+ return detectContentType(value) === 'html';
481
513
  };
482
514
 
483
515
  // Enhanced sanitizer that allows most HTML tags while maintaining security
@@ -636,6 +668,66 @@ const BuilderLikeRenderer: React.FC<{ blocks?: any[] }> = ({ blocks }) => {
636
668
  );
637
669
  };
638
670
 
671
+ // Enhanced code formatter for raw code content
672
+ const CodeFormatter: React.FC<{ content: string }> = ({ content }) => {
673
+ if (!content) return null;
674
+
675
+ // Split content by file separators and format each section
676
+ const sections = content.split(/(---\s*\w+\.\w+\s*---)/g);
677
+
678
+ return (
679
+ <div className="message-container">
680
+ <div className="space-y-4">
681
+ {sections.map((section, index) => {
682
+ if (!section.trim()) return null;
683
+
684
+ // Check if this is a file separator
685
+ const fileMatch = section.match(/---\s*(\w+\.\w+)\s*---/);
686
+ if (fileMatch) {
687
+ return (
688
+ <div key={index} className="code-block-container">
689
+ <div className="code-block-header">📄 {fileMatch[1]}</div>
690
+ </div>
691
+ );
692
+ }
693
+
694
+ // Format the code content
695
+ const lines = section.trim().split('\n');
696
+ const isCode = lines.some(
697
+ (line) =>
698
+ line.includes('@tailwind') ||
699
+ line.includes('export default') ||
700
+ line.includes('import ') ||
701
+ line.includes('{') ||
702
+ line.includes('}') ||
703
+ line.includes('//') ||
704
+ line.includes('/*'),
705
+ );
706
+
707
+ if (isCode) {
708
+ return (
709
+ <div key={index} className="code-block-container">
710
+ <div className="code-block-content">
711
+ <pre className="text-sm text-gray-900 font-mono leading-relaxed whitespace-pre-wrap">
712
+ <code>{section.trim()}</code>
713
+ </pre>
714
+ </div>
715
+ </div>
716
+ );
717
+ }
718
+
719
+ // Regular text content
720
+ return (
721
+ <div key={index} className="text-gray-700 leading-relaxed whitespace-pre-wrap">
722
+ {section.trim()}
723
+ </div>
724
+ );
725
+ })}
726
+ </div>
727
+ </div>
728
+ );
729
+ };
730
+
639
731
  // Enhanced markdown renderer using react-markdown with remark-gfm
640
732
  const FormattedMessageContent: React.FC<{ content: string }> = ({ content }) => {
641
733
  if (!content) return null;
@@ -971,7 +1063,8 @@ const ModernMessageBubble: React.FC<ModernMessageBubbleProps> = ({
971
1063
  >
972
1064
  <div className="flex items-start justify-end gap-2">
973
1065
  {/* Message content and timestamp on the left */}
974
- <div className="flex flex-col items-end max-w-xs lg:max-w-md">
1066
+ {/* <div className="flex flex-col items-end max-w-xs lg:max-w-md"> */}
1067
+ <div className="flex flex-col items-end ">
975
1068
  {!isAssistantRole && (
976
1069
  <div className="flex items-end space-x-2 mb-0.5">
977
1070
  <span className="text-sm font-semibold text-gray-900">{authorName}</span>
@@ -988,16 +1081,24 @@ const ModernMessageBubble: React.FC<ModernMessageBubbleProps> = ({
988
1081
  >
989
1082
  {message.message && (
990
1083
  <div className="max-w-none">
991
- {isProbablyHTML(message.message) ? (
992
- <div
993
- className="text-gray-800 html-content"
994
- dangerouslySetInnerHTML={{
995
- __html: prettifyHtmlContent(sanitizeHtml(message.message)),
996
- }}
997
- />
998
- ) : (
999
- <FormattedMessageContent content={message.message} />
1000
- )}
1084
+ {(() => {
1085
+ const contentType = detectContentType(message.message);
1086
+
1087
+ if (contentType === 'code') {
1088
+ return <CodeFormatter content={message.message} />;
1089
+ } else if (contentType === 'html') {
1090
+ return (
1091
+ <div
1092
+ className="text-gray-800 html-content"
1093
+ dangerouslySetInnerHTML={{
1094
+ __html: prettifyHtmlContent(sanitizeHtml(message.message)),
1095
+ }}
1096
+ />
1097
+ );
1098
+ } else {
1099
+ return <FormattedMessageContent content={message.message} />;
1100
+ }
1101
+ })()}
1001
1102
  </div>
1002
1103
  )}
1003
1104
 
@@ -1058,19 +1159,24 @@ const ModernMessageBubble: React.FC<ModernMessageBubbleProps> = ({
1058
1159
  dir={detectTextDirection((message as any)?.message || '')}
1059
1160
  lang={detectLanguageTag((message as any)?.message || '')}
1060
1161
  >
1061
- {/* Prefer fenced code rendering over HTML when present */}
1062
- {message.message.includes('```') ? (
1063
- <FormattedMessageContent content={message.message} />
1064
- ) : isProbablyHTML(message.message) ? (
1065
- <div
1066
- className="max-w-none text-gray-800 html-content"
1067
- dangerouslySetInnerHTML={{
1068
- __html: prettifyHtmlContent(sanitizeHtml(message.message)),
1069
- }}
1070
- />
1071
- ) : (
1072
- <FormattedMessageContent content={message.message} />
1073
- )}
1162
+ {(() => {
1163
+ const contentType = detectContentType(message.message);
1164
+
1165
+ if (contentType === 'code') {
1166
+ return <CodeFormatter content={message.message} />;
1167
+ } else if (contentType === 'html') {
1168
+ return (
1169
+ <div
1170
+ className="max-w-none text-gray-800 html-content"
1171
+ dangerouslySetInnerHTML={{
1172
+ __html: prettifyHtmlContent(sanitizeHtml(message.message)),
1173
+ }}
1174
+ />
1175
+ );
1176
+ } else {
1177
+ return <FormattedMessageContent content={message.message} />;
1178
+ }
1179
+ })()}
1074
1180
  </div>
1075
1181
  )}
1076
1182
 
@@ -15,7 +15,7 @@ const providerIcons: Record<ModelConfig['provider'], string> = {
15
15
  gemini: '✨',
16
16
  };
17
17
 
18
- const templateOptions = [
18
+ export const templateOptions = [
19
19
  {
20
20
  value: 'vite-react' as const,
21
21
  label: 'Vite React',
@@ -32,7 +32,7 @@ const templateDetails = {
32
32
  'vite-react': { name: 'Vite React', icon: '⚡' },
33
33
  };
34
34
 
35
- const modelOptions: Record<ModelConfig['provider'], { value: string; label: string; description: string }[]> = {
35
+ export const modelOptions: Record<ModelConfig['provider'], { value: string; label: string; description: string }[]> = {
36
36
  openai: [
37
37
  { value: 'gpt-4o', label: 'GPT-4o', description: 'Latest multimodal model' },
38
38
  { value: 'gpt-4o-mini', label: 'GPT-4o mini', description: 'Fast and efficient' },
@@ -72,6 +72,17 @@ const providerDetails = {
72
72
  gemini: { name: 'Google Vertex AI' },
73
73
  };
74
74
 
75
+ // Helper function to get all models from all providers
76
+ export const getAllModels = () => {
77
+ const allModels: { value: string; label: string }[] = [];
78
+ Object.values(modelOptions).forEach((providerModels) => {
79
+ providerModels.forEach((model) => {
80
+ allModels.push({ value: model.value, label: model.label });
81
+ });
82
+ });
83
+ return allModels;
84
+ };
85
+
75
86
  export const ModelConfigPanel: React.FC<ModelConfigPanelProps> = ({
76
87
  config,
77
88
  onConfigChange,