|
1 | 1 | import React, { useState, useRef, useEffect } from 'react' |
2 | 2 | import { Button } from '@/components/ui/button' |
3 | | -import { LucideSend, Search, Upload } from 'lucide-react' |
| 3 | +import { LucideSend, Search, Upload, Image as LucideImage } from 'lucide-react' |
4 | 4 | import * as Tooltip from '@radix-ui/react-tooltip' |
5 | 5 | import { useActiveConversation } from '@/hooks/use-active-conversation' |
6 | 6 | import { useCreateConversation } from '@/hooks/use-create-conversation' |
@@ -39,6 +39,11 @@ type PendingMessage = { |
39 | 39 | messages: { role: string; content: string }[]; |
40 | 40 | }; |
41 | 41 |
|
| 42 | +const isImageModel = (model: string | null | undefined) => { |
| 43 | + if (!model) return false; |
| 44 | + return /flux|image|black-forest-labs/i.test(model); |
| 45 | +}; |
| 46 | + |
42 | 47 | const ChatInput = React.memo(function ChatInput({ onOpenSearch, defaultModel }: { onOpenSearch?: () => void, defaultModel?: string }) { |
43 | 48 | const activeConversationId = useActiveConversation(s => s.activeConversationId) |
44 | 49 | const setActiveConversationId = useActiveConversation(s => s.setActiveConversationId) |
@@ -117,6 +122,52 @@ const ChatInput = React.memo(function ChatInput({ onOpenSearch, defaultModel }: |
117 | 122 | } |
118 | 123 | }, [activeConversationId]); |
119 | 124 |
|
| 125 | + const handleGenerateImage = async () => { |
| 126 | + if (!isImageModel(selectedModel)) { |
| 127 | + toast.error('Current model cannot generate images.'); |
| 128 | + return; |
| 129 | + } |
| 130 | + if (!inputValue.trim()) { |
| 131 | + toast.error('Enter a prompt to generate an image.'); |
| 132 | + return; |
| 133 | + } |
| 134 | + // Get Supabase JWT for secure auth |
| 135 | + const { data: { session } } = await supabase.auth.getSession(); |
| 136 | + const jwt = session?.access_token; |
| 137 | + if (!jwt) { |
| 138 | + toast.error('You must be signed in to generate images.'); |
| 139 | + return; |
| 140 | + } |
| 141 | + try { |
| 142 | + const model = selectedModel ?? ''; |
| 143 | + if (!model) throw new Error('No model selected'); |
| 144 | + const res = await fetch('/api/generate-image', { |
| 145 | + method: 'POST', |
| 146 | + headers: { |
| 147 | + 'Content-Type': 'application/json', |
| 148 | + 'Authorization': `Bearer ${jwt}`, |
| 149 | + }, |
| 150 | + body: JSON.stringify({ prompt: inputValue.trim() }), |
| 151 | + }); |
| 152 | + const data = await res.json(); |
| 153 | + const url = data?.data?.[0]?.url; |
| 154 | + if (!url) throw new Error('No image returned'); |
| 155 | + // Add as a new message in chat (simulate user + assistant) |
| 156 | + setInputValue(''); |
| 157 | + setPendingAttachments([]); |
| 158 | + if (!activeConversationId || typeof activeConversationId !== 'string') { |
| 159 | + toast.error('No active conversation.'); |
| 160 | + return; |
| 161 | + } |
| 162 | + await createMessage.mutateAsync({ conversation_id: activeConversationId, content: inputValue.trim(), role: 'user' }); |
| 163 | + await createMessage.mutateAsync({ conversation_id: activeConversationId, content: ``, role: 'assistant' }); |
| 164 | + toast.success('Image generated!'); |
| 165 | + } catch (err: unknown) { |
| 166 | + const msg = typeof err === 'object' && err && 'message' in err ? (err as { message?: string }).message : String(err); |
| 167 | + toast.error('Image generation failed: ' + (msg || 'Unknown error')); |
| 168 | + } |
| 169 | + }; |
| 170 | + |
120 | 171 | return ( |
121 | 172 | <> |
122 | 173 | {showLimitModal && createPortal( |
@@ -433,36 +484,64 @@ const ChatInput = React.memo(function ChatInput({ onOpenSearch, defaultModel }: |
433 | 484 | </Tooltip.Content> |
434 | 485 | </Tooltip.Portal> |
435 | 486 | </Tooltip.Root> |
436 | | - {[Search].map((Icon, i) => ( |
437 | | - <Tooltip.Root key={i} delayDuration={200}> |
438 | | - <Tooltip.Trigger asChild> |
439 | | - <Button |
440 | | - type="button" |
441 | | - size="icon" |
442 | | - variant="ghost" |
443 | | - className="w-10 h-10 text-[#ececf1] bg-transparent border-none shadow-none hover:bg-transparent focus:bg-transparent active:bg-transparent" |
444 | | - onClick={i === 0 && onOpenSearch ? onOpenSearch : undefined} |
445 | | - > |
446 | | - <Icon size={18} /> |
447 | | - </Button> |
448 | | - </Tooltip.Trigger> |
449 | | - <Tooltip.Portal> |
450 | | - <Tooltip.Content |
451 | | - sideOffset={8} |
452 | | - className="px-3 py-1.5 rounded-md text-xs shadow-lg border z-50" |
453 | | - style={{ |
454 | | - background: "var(--popover)", |
455 | | - color: "var(--popover-foreground)", |
456 | | - borderColor: "var(--border)", |
457 | | - fontFamily: "var(--font-sans)", |
458 | | - }} |
459 | | - > |
460 | | - {["Search"][i]} |
461 | | - <Tooltip.Arrow className="fill-[var(--popover)]" /> |
462 | | - </Tooltip.Content> |
463 | | - </Tooltip.Portal> |
464 | | - </Tooltip.Root> |
465 | | - ))} |
| 487 | + <Tooltip.Root delayDuration={200}> |
| 488 | + <Tooltip.Trigger asChild> |
| 489 | + <Button |
| 490 | + type="button" |
| 491 | + size="icon" |
| 492 | + variant="ghost" |
| 493 | + className="w-10 h-10 text-[#ececf1] bg-transparent border-none shadow-none hover:bg-transparent focus:bg-transparent active:bg-transparent" |
| 494 | + onClick={onOpenSearch} |
| 495 | + aria-label="Search" |
| 496 | + > |
| 497 | + <Search size={18} /> |
| 498 | + </Button> |
| 499 | + </Tooltip.Trigger> |
| 500 | + <Tooltip.Portal> |
| 501 | + <Tooltip.Content |
| 502 | + sideOffset={8} |
| 503 | + className="px-3 py-1.5 rounded-md text-xs shadow-lg border z-50" |
| 504 | + style={{ |
| 505 | + background: "var(--popover)", |
| 506 | + color: "var(--popover-foreground)", |
| 507 | + borderColor: "var(--border)", |
| 508 | + fontFamily: "var(--font-sans)", |
| 509 | + }} |
| 510 | + > |
| 511 | + Search |
| 512 | + <Tooltip.Arrow className="fill-[var(--popover)]" /> |
| 513 | + </Tooltip.Content> |
| 514 | + </Tooltip.Portal> |
| 515 | + </Tooltip.Root> |
| 516 | + <Tooltip.Root delayDuration={200}> |
| 517 | + <Tooltip.Trigger asChild> |
| 518 | + <Button |
| 519 | + type="button" |
| 520 | + size="icon" |
| 521 | + variant="ghost" |
| 522 | + className="w-10 h-10 text-[#ececf1] bg-transparent border-none shadow-none hover:bg-transparent focus:bg-transparent active:bg-transparent" |
| 523 | + onClick={handleGenerateImage} |
| 524 | + aria-label="Generate image" |
| 525 | + > |
| 526 | + <LucideImage size={18} /> |
| 527 | + </Button> |
| 528 | + </Tooltip.Trigger> |
| 529 | + <Tooltip.Portal> |
| 530 | + <Tooltip.Content |
| 531 | + sideOffset={8} |
| 532 | + className="px-3 py-1.5 rounded-md text-xs shadow-lg border z-50" |
| 533 | + style={{ |
| 534 | + background: "var(--popover)", |
| 535 | + color: "var(--popover-foreground)", |
| 536 | + borderColor: "var(--border)", |
| 537 | + fontFamily: "var(--font-sans)", |
| 538 | + }} |
| 539 | + > |
| 540 | + Generate Image |
| 541 | + <Tooltip.Arrow className="fill-[var(--popover)]" /> |
| 542 | + </Tooltip.Content> |
| 543 | + </Tooltip.Portal> |
| 544 | + </Tooltip.Root> |
466 | 545 | </div> |
467 | 546 | <Tooltip.Root delayDuration={200}> |
468 | 547 | <Tooltip.Trigger asChild> |
|
0 commit comments