| import { useState, useRef } from 'react' |
|
|
| const PROVIDER_ICONS = { |
| groq: '⚡', google: '🌐', mistral: '💫', huggingface: '🤗', nvidia: '◆', openrouter: '🔀', |
| } |
|
|
| export default function Composer({ |
| onSend, onUpload, attachments, onRemoveAttachment, |
| provider, model, registry, agentMode, onToggleAgent, |
| onOpenModelPicker, disabled, onClickDisabled |
| }) { |
| const [text, setText] = useState('') |
| const [isDrag, setIsDrag] = useState(false) |
| const fileInputRef = useRef(null) |
| const textareaRef = useRef(null) |
|
|
| const handleInput = (e) => { |
| setText(e.target.value) |
| if (textareaRef.current) { |
| textareaRef.current.style.height = 'auto' |
| textareaRef.current.style.height = Math.min(textareaRef.current.scrollHeight, 180) + 'px' |
| } |
| } |
|
|
| const handleDrop = (e) => { |
| e.preventDefault(); setIsDrag(false) |
| if (disabled) { onClickDisabled(); return } |
| if (e.dataTransfer.files?.length) onUpload(e.dataTransfer.files) |
| } |
| const handleDragOver = (e) => { e.preventDefault(); setIsDrag(true) } |
| const handleDragLeave = (e) => { e.preventDefault(); setIsDrag(false) } |
|
|
| const handleKey = (e) => { |
| if (e.key === 'Enter' && !e.shiftKey) { |
| e.preventDefault() |
| if (disabled) onClickDisabled() |
| else if (text.trim() || attachments.length > 0) { |
| onSend(text) |
| setText('') |
| if (textareaRef.current) textareaRef.current.style.height = '36px' |
| } |
| } |
| } |
|
|
| const handleSendClick = () => { |
| if (disabled) onClickDisabled() |
| else { |
| onSend(text) |
| setText('') |
| if (textareaRef.current) textareaRef.current.style.height = '36px' |
| } |
| } |
|
|
| const handleFileChange = (e) => { |
| if (disabled) { onClickDisabled(); return } |
| if (e.target.files?.length) { |
| onUpload(e.target.files) |
| e.target.value = '' |
| } |
| } |
|
|
| const provRegistry = registry[provider] || { label: 'Unknown', models: [] } |
| const modData = provRegistry.models?.find(m => m.id === model) || { name: model } |
|
|
| return ( |
| <div className="composer-wrap"> |
| <div |
| className="composer" |
| onDrop={handleDrop} onDragOver={handleDragOver} onDragLeave={handleDragLeave} |
| style={isDrag ? { border: '2px dashed var(--b3)', background: 'var(--s2)' } : {}} |
| > |
| {/* Attachments strip */} |
| {attachments.length > 0 && ( |
| <div className="attachments-bar"> |
| {attachments.map((a, i) => ( |
| <div key={i} className="att-chip"> |
| 📎 {a.filename} |
| <button className="att-remove" onClick={() => onRemoveAttachment(i)}>×</button> |
| </div> |
| ))} |
| </div> |
| )} |
| |
| {/* Main input row */} |
| <div className="composer-row"> |
| <input |
| type="file" |
| multiple |
| ref={fileInputRef} |
| style={{ display: 'none' }} |
| onChange={handleFileChange} |
| /> |
| |
| <textarea |
| ref={textareaRef} |
| className="msg-input" |
| placeholder={disabled ? "Log in to start chatting..." : "Message OwnGPT"} |
| value={text} |
| onChange={handleInput} |
| onKeyDown={handleKey} |
| rows={1} |
| disabled={disabled} |
| onClick={disabled ? onClickDisabled : undefined} |
| /> |
| </div> |
| |
| {/* Bottom toolbar row */} |
| <div className="composer-toolbar"> |
| {/* Left: attach + model + agent */} |
| <div className="composer-toolbar-left"> |
| <button className="composer-icon-btn" onClick={() => fileInputRef.current?.click()} title="Attach file"> |
| <svg viewBox="0 0 24 24"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg> |
| </button> |
| |
| <button className="composer-pill-btn" onClick={disabled ? onClickDisabled : onOpenModelPicker} type="button"> |
| {PROVIDER_ICONS[provider] || '🤖'} {provRegistry.label} <span className="composer-sep">/</span> {modData.name} |
| <svg viewBox="0 0 24 24" className="chevron-icon"><polyline points="6 9 12 15 18 9"/></svg> |
| </button> |
| |
| <button |
| className={`composer-pill-btn${agentMode ? ' active' : ''}`} |
| onClick={disabled ? onClickDisabled : onToggleAgent} |
| type="button" |
| > |
| <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg> |
| Coding |
| {agentMode && <span className="badge">ON</span>} |
| <svg viewBox="0 0 24 24" className="chevron-icon"><polyline points="6 9 12 15 18 9"/></svg> |
| </button> |
| </div> |
| |
| {/* Right: mic + send */} |
| <div className="composer-toolbar-right"> |
| <button className="composer-icon-btn" title="Voice input"> |
| <svg viewBox="0 0 24 24"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg> |
| </button> |
| |
| <button |
| className="send-btn" |
| onClick={handleSendClick} |
| disabled={!disabled && !text.trim() && attachments.length === 0} |
| > |
| <svg viewBox="0 0 24 24"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg> |
| </button> |
| </div> |
| </div> |
| </div> |
| |
| <div className="composer-hint"> |
| Use <kbd>Shift</kbd> + <kbd>Enter</kbd> for a new line. OwnGPT can make mistakes. |
| </div> |
| </div> |
| ) |
| } |
|
|