Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
98 changes: 69 additions & 29 deletions src/components/ChatInput.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand All @@ -29,6 +31,7 @@ export default function ChatInput({
onSend,
disabled,
running,
connected = true,
onCancel,
onOpenProfiles,
onOpenMemory,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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.`,
Expand Down Expand Up @@ -583,7 +599,7 @@ export default function ChatInput({
modes={modes}
selected={selectedMode}
onChange={onModeChange}
disabled={disabled}
disabled={busyOrOffline}
/>
)}

Expand All @@ -593,7 +609,7 @@ export default function ChatInput({
selected={selectedProvider}
onChange={onProviderChange}
locked={providerLocked}
disabled={disabled}
disabled={busyOrOffline}
/>
)}

Expand All @@ -610,7 +626,7 @@ export default function ChatInput({
<select
onChange={handlePresetSelect}
defaultValue=""
disabled={disabled}
disabled={busyOrOffline}
title="Insert a saved prompt preset"
className="px-2 py-1.5 bg-gray-800 border border-gray-700 text-gray-300 text-sm
rounded-lg hover:border-gray-600 focus:outline-none cursor-pointer
Expand Down Expand Up @@ -652,7 +668,7 @@ export default function ChatInput({
selected={selectedSkills}
onChange={onSkillsChange}
locked={skillsLocked}
disabled={disabled}
disabled={busyOrOffline}
activeMode={activeMode}
/>
)}
Expand All @@ -664,7 +680,7 @@ export default function ChatInput({
type="button"
onClick={onToggleIncognito}
disabled={
disabled ||
busyOrOffline ||
noHistory ||
selectedMode === "monitor" ||
selectedMode === "scheduler" ||
Expand Down Expand Up @@ -709,12 +725,13 @@ export default function ChatInput({
<button
type="button"
onClick={onToggleNotifications}
disabled={offline}
title={
notificationsEnabled
? "Desktop notifications enabled — click to disable"
: "Enable desktop notifications when runs complete"
}
className={`p-2 rounded-lg transition-colors
className={`p-2 rounded-lg transition-colors disabled:opacity-40 disabled:cursor-not-allowed
${
notificationsEnabled
? "text-amber-400 bg-amber-900/30 border border-amber-600/50 hover:bg-amber-900/50"
Expand Down Expand Up @@ -742,7 +759,7 @@ export default function ChatInput({
<button
type="button"
onClick={onOpenProfiles}
disabled={disabled}
disabled={busyOrOffline}
title="Edit profile overrides"
className="p-2 text-gray-400 hover:text-gray-200 bg-gray-800 border border-gray-700
rounded-lg hover:border-gray-600 transition-colors
Expand All @@ -769,9 +786,11 @@ export default function ChatInput({
<button
type="button"
onClick={onOpenMemory}
disabled={offline}
title="Memory — stored facts and recalled exchanges"
className="p-2 text-gray-400 hover:text-gray-200 bg-gray-800 border border-gray-700
rounded-lg hover:border-gray-600 transition-colors"
rounded-lg hover:border-gray-600 transition-colors
disabled:opacity-40 disabled:cursor-not-allowed"
>
<svg
xmlns="http://www.w3.org/2000/svg"
Expand All @@ -793,9 +812,11 @@ export default function ChatInput({
<button
type="button"
onClick={() => onOpenConnectors?.()}
disabled={offline}
title="Connectors — Gmail, Drive, and other services"
className="p-2 text-gray-400 hover:text-gray-200 bg-gray-800 border border-gray-700
rounded-lg hover:border-gray-600 transition-colors"
rounded-lg hover:border-gray-600 transition-colors
disabled:opacity-40 disabled:cursor-not-allowed"
>
<svg
xmlns="http://www.w3.org/2000/svg"
Expand All @@ -816,9 +837,11 @@ export default function ChatInput({
<button
type="button"
onClick={() => onOpenBookmarks?.()}
disabled={offline}
title="Bookmarks — saved tool calls and answers"
className="p-2 text-gray-400 hover:text-gray-200 bg-gray-800 border border-gray-700
rounded-lg hover:border-gray-600 transition-colors"
rounded-lg hover:border-gray-600 transition-colors
disabled:opacity-40 disabled:cursor-not-allowed"
>
<svg
xmlns="http://www.w3.org/2000/svg"
Expand All @@ -838,9 +861,11 @@ export default function ChatInput({
<button
type="button"
onClick={() => setHelpOpen(true)}
disabled={offline}
title="Cheat sheet — modes & tools"
className="p-2 text-gray-400 hover:text-gray-200 bg-gray-800 border border-gray-700
rounded-lg hover:border-gray-600 transition-colors"
rounded-lg hover:border-gray-600 transition-colors
disabled:opacity-40 disabled:cursor-not-allowed"
>
<svg
xmlns="http://www.w3.org/2000/svg"
Expand Down Expand Up @@ -903,21 +928,23 @@ export default function ChatInput({
onKeyDown={handleKeyDown}
onPaste={handlePaste}
placeholder={
disabled
? "Agent is working..."
: stillUploading
? "Uploading files..."
: hasFiles
? `${pendingFiles.length} file${pendingFiles.length > 1 ? "s" : ""} attached — add a message...`
: noHistory || selectedMode === "cloud" || selectedMode === "gitlab"
? "Private session — follow-ups work, cross-session history skipped..."
: incognito
? "Private session — no history forwarded to the AI..."
: selectedMode === "chat"
? "Ask the agent..."
: `${modes.find((m) => m.id === selectedMode)?.label} mode — describe your task...`
offline
? "Disconnected — reconnecting to AgentForge…"
: disabled
? "Agent is working..."
: stillUploading
? "Uploading files..."
: hasFiles
? `${pendingFiles.length} file${pendingFiles.length > 1 ? "s" : ""} attached — add a message...`
: noHistory || selectedMode === "cloud" || selectedMode === "gitlab"
? "Private session — follow-ups work, cross-session history skipped..."
: incognito
? "Private session — no history forwarded to the AI..."
: selectedMode === "chat"
? "Ask the agent..."
: `${modes.find((m) => m.id === selectedMode)?.label} mode — describe your task...`
}
disabled={disabled}
disabled={busyOrOffline}
autoFocus
className={`flex-1 px-3 py-2 bg-gray-800 rounded-lg
text-gray-100 placeholder-gray-500
Expand Down Expand Up @@ -977,7 +1004,10 @@ export default function ChatInput({
<button
type="submit"
disabled={
disabled || stillUploading || wouldExceedContext || (!text.trim() && !hasFiles)
busyOrOffline ||
stillUploading ||
wouldExceedContext ||
(!text.trim() && !hasFiles)
}
title={wouldExceedContext ? "Content exceeds model context window" : undefined}
className={`px-4 py-2 text-white text-sm font-medium rounded-lg transition-colors
Expand All @@ -1004,9 +1034,19 @@ export default function ChatInput({
<ContextBar
contextUsage={contextUsage || { percent: 0, message_count: 0 }}
onCompact={onCompactSession}
disabled={running}
disabled={busyOrOffline}
/>
</div>

{offline && (
<div
className="mt-3 flex items-center gap-2 px-3 py-1.5 rounded-lg
bg-red-950/40 border border-red-800/40 text-red-300 text-xs"
>
<span className="flex-shrink-0 w-2 h-2 rounded-full bg-red-400 animate-pulse" />
<span className="flex-1">Not connected to AgentForge! Reconnecting…</span>
</div>
)}
</div>

<HelpModal open={helpOpen} onClose={() => setHelpOpen(false)} />
Expand Down
1 change: 1 addition & 0 deletions src/components/ChatPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default function ChatPage() {
<KnowledgeBar />
<ChatView
messages={agent.messages}
connected={agent.connected}
running={agent.running}
confirm={agent.confirm}
secret={agent.secret}
Expand Down
2 changes: 2 additions & 0 deletions src/components/ChatView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const detectModeFromMessages = (messages, aliasToMode) => {

export default function ChatView({
messages,
connected = true,
running,
confirm,
secret,
Expand Down Expand Up @@ -222,6 +223,7 @@ export default function ChatView({
onSend: handleSend,
disabled: running,
running,
connected,
onCancel,
profiles,
onOpenProfiles: () => setModalOpen(true),
Expand Down
9 changes: 8 additions & 1 deletion src/components/Sidebar.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import ws from "../lib/ws";

const fmtTokens = (n) => {
if (!n || n <= 0) return "~";
Expand All @@ -17,7 +18,7 @@ export default function Sidebar({ collapsed, onToggle }) {
const { sessionId: activeSessionId } = useParams();

const fetchSessions = useCallback(() => {
fetch("/api/sessions?limit=100")
fetch("/api/sessions?limit=100&source=web")
.then((r) => (r.ok ? r.json() : []))
.then(setSessions)
.catch(() => {});
Expand All @@ -29,6 +30,12 @@ export default function Sidebar({ collapsed, onToggle }) {
return () => clearInterval(interval);
}, [fetchSessions]);

useEffect(() => {
const onConnected = () => fetchSessions();
ws.on("connected", onConnected);
return () => ws.off("connected", onConnected);
}, [fetchSessions]);

const handleNewChat = () => {
navigate("/");
};
Expand Down
6 changes: 3 additions & 3 deletions src/lib/ws.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ class AgentWS {
*/
_getUrl(sessionId) {
const proto = location.protocol === "https:" ? "wss:" : "ws:";
let url = `${proto}//${location.host}/ws/chat`;
const params = new URLSearchParams({ source: "web" });
if (sessionId) {
url += `?session_id=${encodeURIComponent(sessionId)}`;
params.set("session_id", sessionId);
}
return url;
return `${proto}//${location.host}/ws/chat?${params}`;
}

/**
Expand Down