OwnGPT.v2 / client /src /components /Composer.jsx
parthib07's picture
Upload 200 files
48b8720 verified
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)}>&times;</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>
)
}