diff --git a/package.json b/package.json index 943dfcc..2890eab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-forge-webui", - "version": "0.1.0", + "version": "0.2.0", "description": "React SPA front-end for AgentForge — every mode, tool call, and connector in one chat UI", "type": "module", "license": "MIT", diff --git a/src/components/ChatInput.jsx b/src/components/ChatInput.jsx index 26910c7..e947c25 100644 --- a/src/components/ChatInput.jsx +++ b/src/components/ChatInput.jsx @@ -4,6 +4,8 @@ import ProviderSelector from "./ProviderSelector"; const PROMPT_DRAFT_KEY = "agentforge:prompt-draft"; +const OFFLINE_GRACE_MS = 3000; + const dragHasFiles = (e) => Array.from(e.dataTransfer?.types || []).includes("Files"); const BLOCKED_UPLOAD_EXTS = new Set([ @@ -29,6 +31,7 @@ export default function ChatInput({ onSend, disabled, running, + connected = true, onCancel, onOpenProfiles, onOpenMemory, @@ -68,9 +71,22 @@ export default function ChatInput({ }) { const [text, setText] = useState(() => localStorage.getItem(PROMPT_DRAFT_KEY) || ""); const [helpOpen, setHelpOpen] = useState(false); + const [showOffline, setShowOffline] = useState(false); const inputRef = useRef(null); const fileInputRef = useRef(null); + useEffect(() => { + if (connected) { + setShowOffline(false); + return; + } + const t = setTimeout(() => setShowOffline(true), OFFLINE_GRACE_MS); + return () => clearTimeout(t); + }, [connected]); + + const offline = showOffline; + const busyOrOffline = disabled || offline; + useEffect(() => { if (text) localStorage.setItem(PROMPT_DRAFT_KEY, text); else localStorage.removeItem(PROMPT_DRAFT_KEY); @@ -129,7 +145,7 @@ export default function ChatInput({ const remainingTokens = maxTokens - usedTokens; const wouldExceedContext = estimatedNewTokens > remainingTokens; const contextPercent = Math.min(((usedTokens + estimatedNewTokens) / maxTokens) * 100, 100); - const attachDisabled = disabled || atFileLimit || wouldExceedContext; + const attachDisabled = busyOrOffline || atFileLimit || wouldExceedContext; const autoResize = useCallback(() => { const el = inputRef.current; @@ -141,7 +157,7 @@ export default function ChatInput({ const handleSubmit = (e) => { e.preventDefault(); const trimmed = text.trim(); - if ((!trimmed && !hasFiles) || disabled || stillUploading) return; + if ((!trimmed && !hasFiles) || busyOrOffline || stillUploading) return; if (wouldExceedContext) { setAttachError( `Content too large for context window — estimated ${estimatedNewTokens.toLocaleString()} tokens but only ~${remainingTokens.toLocaleString()} remaining. Remove attachments or shorten your prompt.`, @@ -583,7 +599,7 @@ export default function ChatInput({ modes={modes} selected={selectedMode} onChange={onModeChange} - disabled={disabled} + disabled={busyOrOffline} /> )} @@ -593,7 +609,7 @@ export default function ChatInput({ selected={selectedProvider} onChange={onProviderChange} locked={providerLocked} - disabled={disabled} + disabled={busyOrOffline} /> )} @@ -610,7 +626,7 @@ export default function ChatInput({