;
+ setAnswers(parsed);
+ } else {
+ setAnswers({});
+ }
+ setIsSubmitted(false);
+ } catch {
+ setAnswers({});
+ setIsSubmitted(false);
+ }
+ lastSessionIdRef.current = sessionId;
+ }, [sessionId]);
+
+ useEffect(() => {
+ if (lastSessionIdRef.current !== sessionId) {
+ return;
+ }
+ const storageKey = getStorageKey(sessionId);
+ if (!storageKey) return;
+
+ const hasAnswers = Object.values(answers).some((v) => v.trim());
+ try {
+ if (hasAnswers) {
+ localStorage.setItem(storageKey, JSON.stringify(answers));
+ } else {
+ localStorage.removeItem(storageKey);
+ }
+ } catch {}
+ }, [answers, sessionId]);
function handleAnswerChange(keyword: string, value: string) {
setAnswers((prev) => ({ ...prev, [keyword]: value }));
}
function handleSubmit() {
- // Check if all questions are answered
const allAnswered = questions.every((q) => answers[q.keyword]?.trim());
if (!allAnswered) {
return;
}
+ setIsSubmitted(true);
onSubmitAnswers(answers);
+
+ const storageKey = getStorageKey(sessionId);
+ try {
+ if (storageKey) {
+ localStorage.removeItem(storageKey);
+ }
+ } catch {}
}
const allAnswered = questions.every((q) => answers[q.keyword]?.trim());
+ if (isAnswered || isSubmitted) {
+ return (
+
+
+
+
+
+
+ Answers submitted
+
+
+ Processing your responses...
+
+
+
+
+
+ );
+ }
+
return (
);
}
+
+function getStorageKey(sessionId?: string): string | null {
+ if (!sessionId) return null;
+ return `clarification_answers_${sessionId}`;
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/helpers.tsx
new file mode 100644
index 0000000000..bd47eac051
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/helpers.tsx
@@ -0,0 +1,186 @@
+import type { AgentPreviewResponse } from "@/app/api/__generated__/models/agentPreviewResponse";
+import type { AgentSavedResponse } from "@/app/api/__generated__/models/agentSavedResponse";
+import type { ClarificationNeededResponse } from "@/app/api/__generated__/models/clarificationNeededResponse";
+import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse";
+import type { OperationInProgressResponse } from "@/app/api/__generated__/models/operationInProgressResponse";
+import type { OperationPendingResponse } from "@/app/api/__generated__/models/operationPendingResponse";
+import type { OperationStartedResponse } from "@/app/api/__generated__/models/operationStartedResponse";
+import { ResponseType } from "@/app/api/__generated__/models/responseType";
+import {
+ PlusCircleIcon,
+ PlusIcon,
+ WarningDiamondIcon,
+} from "@phosphor-icons/react";
+import type { ToolUIPart } from "ai";
+import { OrbitLoader } from "../../components/OrbitLoader/OrbitLoader";
+
+export type CreateAgentToolOutput =
+ | OperationStartedResponse
+ | OperationPendingResponse
+ | OperationInProgressResponse
+ | AgentPreviewResponse
+ | AgentSavedResponse
+ | ClarificationNeededResponse
+ | ErrorResponse;
+
+function parseOutput(output: unknown): CreateAgentToolOutput | null {
+ if (!output) return null;
+ if (typeof output === "string") {
+ const trimmed = output.trim();
+ if (!trimmed) return null;
+ try {
+ return parseOutput(JSON.parse(trimmed) as unknown);
+ } catch {
+ return null;
+ }
+ }
+ if (typeof output === "object") {
+ const type = (output as { type?: unknown }).type;
+ if (
+ type === ResponseType.operation_started ||
+ type === ResponseType.operation_pending ||
+ type === ResponseType.operation_in_progress ||
+ type === ResponseType.agent_preview ||
+ type === ResponseType.agent_saved ||
+ type === ResponseType.clarification_needed ||
+ type === ResponseType.error
+ ) {
+ return output as CreateAgentToolOutput;
+ }
+ if ("operation_id" in output && "tool_name" in output)
+ return output as OperationStartedResponse | OperationPendingResponse;
+ if ("tool_call_id" in output) return output as OperationInProgressResponse;
+ if ("agent_json" in output && "agent_name" in output)
+ return output as AgentPreviewResponse;
+ if ("agent_id" in output && "library_agent_id" in output)
+ return output as AgentSavedResponse;
+ if ("questions" in output) return output as ClarificationNeededResponse;
+ if ("error" in output || "details" in output)
+ return output as ErrorResponse;
+ }
+ return null;
+}
+
+export function getCreateAgentToolOutput(
+ part: unknown,
+): CreateAgentToolOutput | null {
+ if (!part || typeof part !== "object") return null;
+ return parseOutput((part as { output?: unknown }).output);
+}
+
+export function isOperationStartedOutput(
+ output: CreateAgentToolOutput,
+): output is OperationStartedResponse {
+ return (
+ output.type === ResponseType.operation_started ||
+ ("operation_id" in output && "tool_name" in output)
+ );
+}
+
+export function isOperationPendingOutput(
+ output: CreateAgentToolOutput,
+): output is OperationPendingResponse {
+ return output.type === ResponseType.operation_pending;
+}
+
+export function isOperationInProgressOutput(
+ output: CreateAgentToolOutput,
+): output is OperationInProgressResponse {
+ return (
+ output.type === ResponseType.operation_in_progress ||
+ "tool_call_id" in output
+ );
+}
+
+export function isAgentPreviewOutput(
+ output: CreateAgentToolOutput,
+): output is AgentPreviewResponse {
+ return output.type === ResponseType.agent_preview || "agent_json" in output;
+}
+
+export function isAgentSavedOutput(
+ output: CreateAgentToolOutput,
+): output is AgentSavedResponse {
+ return (
+ output.type === ResponseType.agent_saved || "agent_page_link" in output
+ );
+}
+
+export function isClarificationNeededOutput(
+ output: CreateAgentToolOutput,
+): output is ClarificationNeededResponse {
+ return (
+ output.type === ResponseType.clarification_needed || "questions" in output
+ );
+}
+
+export function isErrorOutput(
+ output: CreateAgentToolOutput,
+): output is ErrorResponse {
+ return output.type === ResponseType.error || "error" in output;
+}
+
+export function getAnimationText(part: {
+ state: ToolUIPart["state"];
+ input?: unknown;
+ output?: unknown;
+}): string {
+ switch (part.state) {
+ case "input-streaming":
+ case "input-available":
+ return "Creating a new agent";
+ case "output-available": {
+ const output = parseOutput(part.output);
+ if (!output) return "Creating a new agent";
+ if (isOperationStartedOutput(output)) return "Agent creation started";
+ if (isOperationPendingOutput(output)) return "Agent creation in progress";
+ if (isOperationInProgressOutput(output))
+ return "Agent creation already in progress";
+ if (isAgentSavedOutput(output)) return `Saved "${output.agent_name}"`;
+ if (isAgentPreviewOutput(output)) return `Preview "${output.agent_name}"`;
+ if (isClarificationNeededOutput(output)) return "Needs clarification";
+ return "Error creating agent";
+ }
+ case "output-error":
+ return "Error creating agent";
+ default:
+ return "Creating a new agent";
+ }
+}
+
+export function ToolIcon({
+ isStreaming,
+ isError,
+}: {
+ isStreaming?: boolean;
+ isError?: boolean;
+}) {
+ if (isError) {
+ return (
+
+ );
+ }
+ if (isStreaming) {
+ return
;
+ }
+ return
;
+}
+
+export function AccordionIcon() {
+ return
;
+}
+
+export function formatMaybeJson(value: unknown): string {
+ if (typeof value === "string") return value;
+ try {
+ return JSON.stringify(value, null, 2);
+ } catch {
+ return String(value);
+ }
+}
+
+export function truncateText(text: string, maxChars: number): string {
+ const trimmed = text.trim();
+ if (trimmed.length <= maxChars) return trimmed;
+ return `${trimmed.slice(0, maxChars).trimEnd()}…`;
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/EditAgent/EditAgent.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/EditAgent/EditAgent.tsx
new file mode 100644
index 0000000000..6766a5cb49
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/EditAgent/EditAgent.tsx
@@ -0,0 +1,231 @@
+"use client";
+
+import { WarningDiamondIcon } from "@phosphor-icons/react";
+import type { ToolUIPart } from "ai";
+import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions";
+import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
+import { OrbitLoader } from "../../components/OrbitLoader/OrbitLoader";
+import { ProgressBar } from "../../components/ProgressBar/ProgressBar";
+import {
+ ContentCardDescription,
+ ContentCodeBlock,
+ ContentGrid,
+ ContentHint,
+ ContentLink,
+ ContentMessage,
+} from "../../components/ToolAccordion/AccordionContent";
+import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
+import { useAsymptoticProgress } from "../../hooks/useAsymptoticProgress";
+import {
+ ClarificationQuestionsCard,
+ ClarifyingQuestion,
+} from "../CreateAgent/components/ClarificationQuestionsCard";
+import {
+ AccordionIcon,
+ formatMaybeJson,
+ getAnimationText,
+ getEditAgentToolOutput,
+ isAgentPreviewOutput,
+ isAgentSavedOutput,
+ isClarificationNeededOutput,
+ isErrorOutput,
+ isOperationInProgressOutput,
+ isOperationPendingOutput,
+ isOperationStartedOutput,
+ ToolIcon,
+ truncateText,
+ type EditAgentToolOutput,
+} from "./helpers";
+
+export interface EditAgentToolPart {
+ type: string;
+ toolCallId: string;
+ state: ToolUIPart["state"];
+ input?: unknown;
+ output?: unknown;
+}
+
+interface Props {
+ part: EditAgentToolPart;
+}
+
+function getAccordionMeta(output: EditAgentToolOutput): {
+ icon: React.ReactNode;
+ title: string;
+ titleClassName?: string;
+ description?: string;
+} {
+ const icon =
;
+
+ if (isAgentSavedOutput(output)) {
+ return { icon, title: output.agent_name };
+ }
+ if (isAgentPreviewOutput(output)) {
+ return {
+ icon,
+ title: output.agent_name,
+ description: `${output.node_count} block${output.node_count === 1 ? "" : "s"}`,
+ };
+ }
+ if (isClarificationNeededOutput(output)) {
+ const questions = output.questions ?? [];
+ return {
+ icon,
+ title: "Needs clarification",
+ description: `${questions.length} question${questions.length === 1 ? "" : "s"}`,
+ };
+ }
+ if (
+ isOperationStartedOutput(output) ||
+ isOperationPendingOutput(output) ||
+ isOperationInProgressOutput(output)
+ ) {
+ return { icon:
, title: "Editing agent" };
+ }
+ return {
+ icon: (
+
+ ),
+ title: "Error",
+ titleClassName: "text-red-500",
+ };
+}
+
+export function EditAgentTool({ part }: Props) {
+ const text = getAnimationText(part);
+ const { onSend } = useCopilotChatActions();
+ const isStreaming =
+ part.state === "input-streaming" || part.state === "input-available";
+
+ const output = getEditAgentToolOutput(part);
+ const isError =
+ part.state === "output-error" || (!!output && isErrorOutput(output));
+ const isOperating =
+ !!output &&
+ (isOperationStartedOutput(output) ||
+ isOperationPendingOutput(output) ||
+ isOperationInProgressOutput(output));
+ const progress = useAsymptoticProgress(isOperating);
+ const hasExpandableContent =
+ part.state === "output-available" &&
+ !!output &&
+ (isOperationStartedOutput(output) ||
+ isOperationPendingOutput(output) ||
+ isOperationInProgressOutput(output) ||
+ isAgentPreviewOutput(output) ||
+ isAgentSavedOutput(output) ||
+ isClarificationNeededOutput(output) ||
+ isErrorOutput(output));
+
+ function handleClarificationAnswers(answers: Record
) {
+ const questions =
+ output && isClarificationNeededOutput(output)
+ ? (output.questions ?? [])
+ : [];
+
+ const contextMessage = questions
+ .map((q) => {
+ const answer = answers[q.keyword] || "";
+ return `> ${q.question}\n\n${answer}`;
+ })
+ .join("\n\n");
+
+ onSend(
+ `**Here are my answers:**\n\n${contextMessage}\n\nPlease proceed with editing the agent.`,
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ {hasExpandableContent && output && (
+
+ {isOperating && (
+
+
+
+ This could take a few minutes, grab a coffee ☕
+
+
+ )}
+
+ {isAgentSavedOutput(output) && (
+
+ {output.message}
+
+
+ Open in library
+
+
+ Open in builder
+
+
+
+ {truncateText(
+ formatMaybeJson({ agent_id: output.agent_id }),
+ 800,
+ )}
+
+
+ )}
+
+ {isAgentPreviewOutput(output) && (
+
+ {output.message}
+ {output.description?.trim() && (
+
+ {output.description}
+
+ )}
+
+ {truncateText(formatMaybeJson(output.agent_json), 1600)}
+
+
+ )}
+
+ {isClarificationNeededOutput(output) && (
+ {
+ const item: ClarifyingQuestion = {
+ question: q.question,
+ keyword: q.keyword,
+ };
+ const example =
+ typeof q.example === "string" && q.example.trim()
+ ? q.example.trim()
+ : null;
+ if (example) item.example = example;
+ return item;
+ })}
+ message={output.message}
+ onSubmitAnswers={handleClarificationAnswers}
+ />
+ )}
+
+ {isErrorOutput(output) && (
+
+ {output.message}
+ {output.error && (
+
+ {formatMaybeJson(output.error)}
+
+ )}
+ {output.details && (
+
+ {formatMaybeJson(output.details)}
+
+ )}
+
+ )}
+
+ )}
+
+ );
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/EditAgent/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/EditAgent/helpers.tsx
new file mode 100644
index 0000000000..a0db50cddc
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/EditAgent/helpers.tsx
@@ -0,0 +1,188 @@
+import type { AgentPreviewResponse } from "@/app/api/__generated__/models/agentPreviewResponse";
+import type { AgentSavedResponse } from "@/app/api/__generated__/models/agentSavedResponse";
+import type { ClarificationNeededResponse } from "@/app/api/__generated__/models/clarificationNeededResponse";
+import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse";
+import type { OperationInProgressResponse } from "@/app/api/__generated__/models/operationInProgressResponse";
+import type { OperationPendingResponse } from "@/app/api/__generated__/models/operationPendingResponse";
+import type { OperationStartedResponse } from "@/app/api/__generated__/models/operationStartedResponse";
+import { ResponseType } from "@/app/api/__generated__/models/responseType";
+import {
+ NotePencilIcon,
+ PencilLineIcon,
+ WarningDiamondIcon,
+} from "@phosphor-icons/react";
+import type { ToolUIPart } from "ai";
+import { OrbitLoader } from "../../components/OrbitLoader/OrbitLoader";
+
+export type EditAgentToolOutput =
+ | OperationStartedResponse
+ | OperationPendingResponse
+ | OperationInProgressResponse
+ | AgentPreviewResponse
+ | AgentSavedResponse
+ | ClarificationNeededResponse
+ | ErrorResponse;
+
+function parseOutput(output: unknown): EditAgentToolOutput | null {
+ if (!output) return null;
+ if (typeof output === "string") {
+ const trimmed = output.trim();
+ if (!trimmed) return null;
+ try {
+ return parseOutput(JSON.parse(trimmed) as unknown);
+ } catch {
+ return null;
+ }
+ }
+ if (typeof output === "object") {
+ const type = (output as { type?: unknown }).type;
+ if (
+ type === ResponseType.operation_started ||
+ type === ResponseType.operation_pending ||
+ type === ResponseType.operation_in_progress ||
+ type === ResponseType.agent_preview ||
+ type === ResponseType.agent_saved ||
+ type === ResponseType.clarification_needed ||
+ type === ResponseType.error
+ ) {
+ return output as EditAgentToolOutput;
+ }
+ if ("operation_id" in output && "tool_name" in output)
+ return output as OperationStartedResponse | OperationPendingResponse;
+ if ("tool_call_id" in output) return output as OperationInProgressResponse;
+ if ("agent_json" in output && "agent_name" in output)
+ return output as AgentPreviewResponse;
+ if ("agent_id" in output && "library_agent_id" in output)
+ return output as AgentSavedResponse;
+ if ("questions" in output) return output as ClarificationNeededResponse;
+ if ("error" in output || "details" in output)
+ return output as ErrorResponse;
+ }
+ return null;
+}
+
+export function getEditAgentToolOutput(
+ part: unknown,
+): EditAgentToolOutput | null {
+ if (!part || typeof part !== "object") return null;
+ return parseOutput((part as { output?: unknown }).output);
+}
+
+export function isOperationStartedOutput(
+ output: EditAgentToolOutput,
+): output is OperationStartedResponse {
+ return (
+ output.type === ResponseType.operation_started ||
+ ("operation_id" in output && "tool_name" in output)
+ );
+}
+
+export function isOperationPendingOutput(
+ output: EditAgentToolOutput,
+): output is OperationPendingResponse {
+ return output.type === ResponseType.operation_pending;
+}
+
+export function isOperationInProgressOutput(
+ output: EditAgentToolOutput,
+): output is OperationInProgressResponse {
+ return (
+ output.type === ResponseType.operation_in_progress ||
+ "tool_call_id" in output
+ );
+}
+
+export function isAgentPreviewOutput(
+ output: EditAgentToolOutput,
+): output is AgentPreviewResponse {
+ return output.type === ResponseType.agent_preview || "agent_json" in output;
+}
+
+export function isAgentSavedOutput(
+ output: EditAgentToolOutput,
+): output is AgentSavedResponse {
+ return (
+ output.type === ResponseType.agent_saved || "agent_page_link" in output
+ );
+}
+
+export function isClarificationNeededOutput(
+ output: EditAgentToolOutput,
+): output is ClarificationNeededResponse {
+ return (
+ output.type === ResponseType.clarification_needed || "questions" in output
+ );
+}
+
+export function isErrorOutput(
+ output: EditAgentToolOutput,
+): output is ErrorResponse {
+ return output.type === ResponseType.error || "error" in output;
+}
+
+export function getAnimationText(part: {
+ state: ToolUIPart["state"];
+ input?: unknown;
+ output?: unknown;
+}): string {
+ switch (part.state) {
+ case "input-streaming":
+ case "input-available":
+ return "Editing the agent";
+ case "output-available": {
+ const output = parseOutput(part.output);
+ if (!output) return "Editing the agent";
+ if (isOperationStartedOutput(output)) return "Agent update started";
+ if (isOperationPendingOutput(output)) return "Agent update in progress";
+ if (isOperationInProgressOutput(output))
+ return "Agent update already in progress";
+ if (isAgentSavedOutput(output)) return `Saved "${output.agent_name}"`;
+ if (isAgentPreviewOutput(output)) return `Preview "${output.agent_name}"`;
+ if (isClarificationNeededOutput(output)) return "Needs clarification";
+ return "Error editing agent";
+ }
+ case "output-error":
+ return "Error editing agent";
+ default:
+ return "Editing the agent";
+ }
+}
+
+export function ToolIcon({
+ isStreaming,
+ isError,
+}: {
+ isStreaming?: boolean;
+ isError?: boolean;
+}) {
+ if (isError) {
+ return (
+
+ );
+ }
+ if (isStreaming) {
+ return ;
+ }
+ return (
+
+ );
+}
+
+export function AccordionIcon() {
+ return ;
+}
+
+export function formatMaybeJson(value: unknown): string {
+ if (typeof value === "string") return value;
+ try {
+ return JSON.stringify(value, null, 2);
+ } catch {
+ return String(value);
+ }
+}
+
+export function truncateText(text: string, maxChars: number): string {
+ const trimmed = text.trim();
+ if (trimmed.length <= maxChars) return trimmed;
+ return `${trimmed.slice(0, maxChars).trimEnd()}…`;
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/FindAgents/FindAgents.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/FindAgents/FindAgents.tsx
new file mode 100644
index 0000000000..4f0068b2c5
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/FindAgents/FindAgents.tsx
@@ -0,0 +1,127 @@
+"use client";
+
+import { ToolUIPart } from "ai";
+import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
+import {
+ ContentBadge,
+ ContentCard,
+ ContentCardDescription,
+ ContentCardHeader,
+ ContentCardTitle,
+ ContentGrid,
+ ContentLink,
+} from "../../components/ToolAccordion/AccordionContent";
+import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
+import {
+ AccordionIcon,
+ getAgentHref,
+ getAnimationText,
+ getFindAgentsOutput,
+ getSourceLabelFromToolType,
+ isAgentsFoundOutput,
+ isErrorOutput,
+ ToolIcon,
+} from "./helpers";
+
+export interface FindAgentsToolPart {
+ type: string;
+ toolCallId: string;
+ state: ToolUIPart["state"];
+ input?: unknown;
+ output?: unknown;
+}
+
+interface Props {
+ part: FindAgentsToolPart;
+}
+
+export function FindAgentsTool({ part }: Props) {
+ const text = getAnimationText(part);
+ const output = getFindAgentsOutput(part);
+ const isStreaming =
+ part.state === "input-streaming" || part.state === "input-available";
+ const isError =
+ part.state === "output-error" || (!!output && isErrorOutput(output));
+
+ const query =
+ typeof part.input === "object" && part.input !== null
+ ? String((part.input as { query?: unknown }).query ?? "").trim()
+ : "";
+
+ const agentsFoundOutput =
+ part.state === "output-available" && output && isAgentsFoundOutput(output)
+ ? output
+ : null;
+
+ const hasAgents =
+ !!agentsFoundOutput &&
+ agentsFoundOutput.agents.length > 0 &&
+ (typeof agentsFoundOutput.count !== "number" ||
+ agentsFoundOutput.count > 0);
+ const totalCount = agentsFoundOutput ? agentsFoundOutput.count : 0;
+ const { source } = getSourceLabelFromToolType(part.type);
+ const scopeText =
+ source === "library"
+ ? "in your library"
+ : source === "marketplace"
+ ? "in marketplace"
+ : "";
+ const accordionDescription = `Found ${totalCount}${scopeText ? ` ${scopeText}` : ""}${
+ query ? ` for "${query}"` : ""
+ }`;
+
+ return (
+
+
+
+
+
+
+ {hasAgents && agentsFoundOutput && (
+
}
+ title="Agent results"
+ description={accordionDescription}
+ >
+
+ {agentsFoundOutput.agents.map((agent) => {
+ const href = getAgentHref(agent);
+ const agentSource =
+ agent.source === "library"
+ ? "Library"
+ : agent.source === "marketplace"
+ ? "Marketplace"
+ : null;
+ return (
+
+ Open : null
+ }
+ >
+
+ {agent.name}
+ {agentSource && (
+ {agentSource}
+ )}
+
+
+ {agent.description}
+
+
+
+ );
+ })}
+
+
+ )}
+
+ );
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/FindAgents/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/FindAgents/helpers.tsx
new file mode 100644
index 0000000000..f253947953
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/FindAgents/helpers.tsx
@@ -0,0 +1,187 @@
+import type { AgentInfo } from "@/app/api/__generated__/models/agentInfo";
+import type { AgentsFoundResponse } from "@/app/api/__generated__/models/agentsFoundResponse";
+import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse";
+import type { NoResultsResponse } from "@/app/api/__generated__/models/noResultsResponse";
+import { ResponseType } from "@/app/api/__generated__/models/responseType";
+import {
+ FolderOpenIcon,
+ MagnifyingGlassIcon,
+ SquaresFourIcon,
+ StorefrontIcon,
+} from "@phosphor-icons/react";
+import { ToolUIPart } from "ai";
+
+export interface FindAgentInput {
+ query: string;
+}
+
+export type FindAgentsOutput =
+ | AgentsFoundResponse
+ | NoResultsResponse
+ | ErrorResponse;
+
+export type FindAgentsToolType =
+ | "tool-find_agent"
+ | "tool-find_library_agent"
+ | (string & {});
+
+function parseOutput(output: unknown): FindAgentsOutput | null {
+ if (!output) return null;
+ if (typeof output === "string") {
+ const trimmed = output.trim();
+ if (!trimmed) return null;
+ try {
+ return parseOutput(JSON.parse(trimmed) as unknown);
+ } catch {
+ return null;
+ }
+ }
+ if (typeof output === "object") {
+ const type = (output as { type?: unknown }).type;
+ if (
+ type === ResponseType.agents_found ||
+ type === ResponseType.no_results ||
+ type === ResponseType.error
+ ) {
+ return output as FindAgentsOutput;
+ }
+ if ("agents" in output && "count" in output)
+ return output as AgentsFoundResponse;
+ if ("suggestions" in output && !("error" in output))
+ return output as NoResultsResponse;
+ if ("error" in output || "details" in output)
+ return output as ErrorResponse;
+ }
+ return null;
+}
+
+export function getFindAgentsOutput(part: unknown): FindAgentsOutput | null {
+ if (!part || typeof part !== "object") return null;
+ return parseOutput((part as { output?: unknown }).output);
+}
+
+export function isAgentsFoundOutput(
+ output: FindAgentsOutput,
+): output is AgentsFoundResponse {
+ return output.type === ResponseType.agents_found || "agents" in output;
+}
+
+export function isNoResultsOutput(
+ output: FindAgentsOutput,
+): output is NoResultsResponse {
+ return (
+ output.type === ResponseType.no_results ||
+ ("suggestions" in output && !("error" in output))
+ );
+}
+
+export function isErrorOutput(
+ output: FindAgentsOutput,
+): output is ErrorResponse {
+ return output.type === ResponseType.error || "error" in output;
+}
+
+export function getSourceLabelFromToolType(toolType?: FindAgentsToolType): {
+ source: "marketplace" | "library" | "unknown";
+ label: string;
+} {
+ if (toolType === "tool-find_library_agent") {
+ return { source: "library", label: "Library" };
+ }
+ if (toolType === "tool-find_agent") {
+ return { source: "marketplace", label: "Marketplace" };
+ }
+ return { source: "unknown", label: "Agents" };
+}
+
+export function getAnimationText(part: {
+ type?: FindAgentsToolType;
+ state: ToolUIPart["state"];
+ input?: unknown;
+ output?: unknown;
+}): string {
+ const { source } = getSourceLabelFromToolType(part.type);
+ const query = (part.input as FindAgentInput | undefined)?.query?.trim();
+
+ // Action phrase matching legacy ToolCallMessage
+ const actionPhrase =
+ source === "library"
+ ? "Looking for library agents"
+ : "Looking for agents in the marketplace";
+
+ const queryText = query ? ` matching "${query}"` : "";
+
+ switch (part.state) {
+ case "input-streaming":
+ case "input-available":
+ return `${actionPhrase}${queryText}`;
+
+ case "output-available": {
+ const output = parseOutput(part.output);
+ if (!output) {
+ return `${actionPhrase}${queryText}`;
+ }
+ if (isNoResultsOutput(output)) {
+ return `No agents found${queryText}`;
+ }
+ if (isAgentsFoundOutput(output)) {
+ const count = output.count ?? output.agents?.length ?? 0;
+ return `Found ${count} agent${count === 1 ? "" : "s"}${queryText}`;
+ }
+ if (isErrorOutput(output)) {
+ return `Error finding agents${queryText}`;
+ }
+ return `${actionPhrase}${queryText}`;
+ }
+
+ case "output-error":
+ return `Error finding agents${queryText}`;
+
+ default:
+ return actionPhrase;
+ }
+}
+
+export function getAgentHref(agent: AgentInfo): string | null {
+ if (agent.source === "library") {
+ return `/library/agents/${encodeURIComponent(agent.id)}`;
+ }
+
+ const [creator, slug, ...rest] = agent.id.split("/");
+ if (!creator || !slug || rest.length > 0) return null;
+ return `/marketplace/agent/${encodeURIComponent(creator)}/${encodeURIComponent(slug)}`;
+}
+
+export function ToolIcon({
+ toolType,
+ isStreaming,
+ isError,
+}: {
+ toolType?: FindAgentsToolType;
+ isStreaming?: boolean;
+ isError?: boolean;
+}) {
+ const { source } = getSourceLabelFromToolType(toolType);
+ const IconComponent =
+ source === "library" ? MagnifyingGlassIcon : SquaresFourIcon;
+
+ return (
+
+ );
+}
+
+export function AccordionIcon({ toolType }: { toolType?: FindAgentsToolType }) {
+ const { source } = getSourceLabelFromToolType(toolType);
+ const IconComponent = source === "library" ? FolderOpenIcon : StorefrontIcon;
+ return ;
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/FindBlocks/FindBlocks.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/FindBlocks/FindBlocks.tsx
new file mode 100644
index 0000000000..3684a2da14
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/FindBlocks/FindBlocks.tsx
@@ -0,0 +1,92 @@
+"use client";
+
+import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
+import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
+import {
+ ContentCard,
+ ContentCardDescription,
+ ContentCardTitle,
+} from "../../components/ToolAccordion/AccordionContent";
+import type { BlockListResponse } from "@/app/api/__generated__/models/blockListResponse";
+import type { BlockInfoSummary } from "@/app/api/__generated__/models/blockInfoSummary";
+import { ToolUIPart } from "ai";
+import { HorizontalScroll } from "@/app/(platform)/build/components/NewControlPanel/NewBlockMenu/HorizontalScroll";
+import {
+ AccordionIcon,
+ getAnimationText,
+ parseOutput,
+ ToolIcon,
+} from "./helpers";
+
+export interface FindBlockInput {
+ query: string;
+}
+
+export type FindBlockOutput = BlockListResponse;
+
+export interface FindBlockToolPart {
+ type: string;
+ toolName?: string;
+ toolCallId: string;
+ state: ToolUIPart["state"];
+ input?: FindBlockInput | unknown;
+ output?: string | FindBlockOutput | unknown;
+ title?: string;
+}
+
+interface Props {
+ part: FindBlockToolPart;
+}
+
+function BlockCard({ block }: { block: BlockInfoSummary }) {
+ return (
+
+ {block.name}
+
+ {block.description}
+
+
+ );
+}
+
+export function FindBlocksTool({ part }: Props) {
+ const text = getAnimationText(part);
+ const isStreaming =
+ part.state === "input-streaming" || part.state === "input-available";
+ const isError = part.state === "output-error";
+
+ const parsed =
+ part.state === "output-available" ? parseOutput(part.output) : null;
+ const hasBlocks = !!parsed && parsed.blocks.length > 0;
+
+ const query = (part.input as FindBlockInput | undefined)?.query?.trim();
+ const accordionDescription = parsed
+ ? `Found ${parsed.count} block${parsed.count === 1 ? "" : "s"}${query ? ` for "${query}"` : ""}`
+ : undefined;
+
+ return (
+
+
+
+
+
+
+ {hasBlocks && parsed && (
+
}
+ title="Block results"
+ description={accordionDescription}
+ >
+
+ {parsed.blocks.map((block) => (
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/FindBlocks/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/FindBlocks/helpers.tsx
new file mode 100644
index 0000000000..eaebe98ea5
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/FindBlocks/helpers.tsx
@@ -0,0 +1,75 @@
+import type { BlockListResponse } from "@/app/api/__generated__/models/blockListResponse";
+import { ResponseType } from "@/app/api/__generated__/models/responseType";
+import { CubeIcon, PackageIcon } from "@phosphor-icons/react";
+import { FindBlockInput, FindBlockToolPart } from "./FindBlocks";
+
+export function parseOutput(output: unknown): BlockListResponse | null {
+ if (!output) return null;
+ if (typeof output === "string") {
+ const trimmed = output.trim();
+ if (!trimmed) return null;
+ try {
+ return parseOutput(JSON.parse(trimmed) as unknown);
+ } catch {
+ return null;
+ }
+ }
+ if (typeof output === "object") {
+ const type = (output as { type?: unknown }).type;
+ if (type === ResponseType.block_list || "blocks" in output) {
+ return output as BlockListResponse;
+ }
+ }
+ return null;
+}
+
+export function getAnimationText(part: FindBlockToolPart): string {
+ const query = (part.input as FindBlockInput | undefined)?.query?.trim();
+ const queryText = query ? ` matching "${query}"` : "";
+
+ switch (part.state) {
+ case "input-streaming":
+ case "input-available":
+ return `Searching for blocks${queryText}`;
+
+ case "output-available": {
+ const parsed = parseOutput(part.output);
+ if (parsed) {
+ return `Found ${parsed.count} block${parsed.count === 1 ? "" : "s"}${queryText}`;
+ }
+ return `Searching for blocks${queryText}`;
+ }
+
+ case "output-error":
+ return `Error finding blocks${queryText}`;
+
+ default:
+ return "Searching for blocks";
+ }
+}
+
+export function ToolIcon({
+ isStreaming,
+ isError,
+}: {
+ isStreaming?: boolean;
+ isError?: boolean;
+}) {
+ return (
+
+ );
+}
+
+export function AccordionIcon() {
+ return ;
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/RunAgent.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/RunAgent.tsx
new file mode 100644
index 0000000000..f16b9d2b2f
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/RunAgent.tsx
@@ -0,0 +1,86 @@
+"use client";
+
+import type { ToolUIPart } from "ai";
+import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
+import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
+import { ContentMessage } from "../../components/ToolAccordion/AccordionContent";
+import {
+ getAccordionMeta,
+ getAnimationText,
+ getRunAgentToolOutput,
+ isRunAgentAgentDetailsOutput,
+ isRunAgentErrorOutput,
+ isRunAgentExecutionStartedOutput,
+ isRunAgentNeedLoginOutput,
+ isRunAgentSetupRequirementsOutput,
+ ToolIcon,
+} from "./helpers";
+import { ExecutionStartedCard } from "./components/ExecutionStartedCard/ExecutionStartedCard";
+import { AgentDetailsCard } from "./components/AgentDetailsCard/AgentDetailsCard";
+import { SetupRequirementsCard } from "./components/SetupRequirementsCard/SetupRequirementsCard";
+import { ErrorCard } from "./components/ErrorCard/ErrorCard";
+
+export interface RunAgentToolPart {
+ type: string;
+ toolCallId: string;
+ state: ToolUIPart["state"];
+ input?: unknown;
+ output?: unknown;
+}
+
+interface Props {
+ part: RunAgentToolPart;
+}
+
+export function RunAgentTool({ part }: Props) {
+ const text = getAnimationText(part);
+ const isStreaming =
+ part.state === "input-streaming" || part.state === "input-available";
+
+ const output = getRunAgentToolOutput(part);
+ const isError =
+ part.state === "output-error" ||
+ (!!output && isRunAgentErrorOutput(output));
+ const hasExpandableContent =
+ part.state === "output-available" &&
+ !!output &&
+ (isRunAgentExecutionStartedOutput(output) ||
+ isRunAgentAgentDetailsOutput(output) ||
+ isRunAgentSetupRequirementsOutput(output) ||
+ isRunAgentNeedLoginOutput(output) ||
+ isRunAgentErrorOutput(output));
+
+ return (
+
+
+
+
+
+
+ {hasExpandableContent && output && (
+
+ {isRunAgentExecutionStartedOutput(output) && (
+
+ )}
+
+ {isRunAgentAgentDetailsOutput(output) && (
+
+ )}
+
+ {isRunAgentSetupRequirementsOutput(output) && (
+
+ )}
+
+ {isRunAgentNeedLoginOutput(output) && (
+ {output.message}
+ )}
+
+ {isRunAgentErrorOutput(output) && }
+
+ )}
+
+ );
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/components/AgentDetailsCard/AgentDetailsCard.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/components/AgentDetailsCard/AgentDetailsCard.tsx
new file mode 100644
index 0000000000..f18568faec
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/components/AgentDetailsCard/AgentDetailsCard.tsx
@@ -0,0 +1,116 @@
+"use client";
+
+import type { AgentDetailsResponse } from "@/app/api/__generated__/models/agentDetailsResponse";
+import { Button } from "@/components/atoms/Button/Button";
+import { Text } from "@/components/atoms/Text/Text";
+import { FormRenderer } from "@/components/renderers/InputRenderer/FormRenderer";
+import { AnimatePresence, motion } from "framer-motion";
+import { useState } from "react";
+import { useCopilotChatActions } from "../../../../components/CopilotChatActionsProvider/useCopilotChatActions";
+import { ContentMessage } from "../../../../components/ToolAccordion/AccordionContent";
+import { buildInputSchema } from "./helpers";
+
+interface Props {
+ output: AgentDetailsResponse;
+}
+
+export function AgentDetailsCard({ output }: Props) {
+ const { onSend } = useCopilotChatActions();
+ const [showInputForm, setShowInputForm] = useState(false);
+ const [inputValues, setInputValues] = useState>({});
+
+ function handleRunWithExamples() {
+ onSend(
+ `Run the agent "${output.agent.name}" with placeholder/example values so I can test it.`,
+ );
+ }
+
+ function handleRunWithInputs() {
+ const nonEmpty = Object.fromEntries(
+ Object.entries(inputValues).filter(
+ ([, v]) => v !== undefined && v !== null && v !== "",
+ ),
+ );
+ onSend(
+ `Run the agent "${output.agent.name}" with these inputs: ${JSON.stringify(nonEmpty, null, 2)}`,
+ );
+ setShowInputForm(false);
+ setInputValues({});
+ }
+
+ return (
+
+
+ Run this agent with example values or your own inputs.
+
+
+
+
+
+
+
+
+ {showInputForm && buildInputSchema(output.agent.inputs) && (
+
+
+
Enter your inputs
+
setInputValues(v.formData ?? {})}
+ uiSchema={{
+ "ui:submitButtonOptions": { norender: true },
+ }}
+ initialValues={inputValues}
+ formContext={{
+ showHandles: false,
+ size: "small",
+ }}
+ />
+
+
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/components/AgentDetailsCard/helpers.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/components/AgentDetailsCard/helpers.ts
new file mode 100644
index 0000000000..635b8d20d7
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/components/AgentDetailsCard/helpers.ts
@@ -0,0 +1,8 @@
+import type { RJSFSchema } from "@rjsf/utils";
+
+export function buildInputSchema(inputs: unknown): RJSFSchema | null {
+ if (!inputs || typeof inputs !== "object") return null;
+ const properties = inputs as RJSFSchema["properties"];
+ if (!properties || Object.keys(properties).length === 0) return null;
+ return inputs as RJSFSchema;
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/components/ErrorCard/ErrorCard.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/components/ErrorCard/ErrorCard.tsx
new file mode 100644
index 0000000000..7990428947
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/components/ErrorCard/ErrorCard.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse";
+import {
+ ContentCodeBlock,
+ ContentGrid,
+ ContentMessage,
+} from "../../../../components/ToolAccordion/AccordionContent";
+import { formatMaybeJson } from "../../helpers";
+
+interface Props {
+ output: ErrorResponse;
+}
+
+export function ErrorCard({ output }: Props) {
+ return (
+
+ {output.message}
+ {output.error && (
+ {formatMaybeJson(output.error)}
+ )}
+ {output.details && (
+ {formatMaybeJson(output.details)}
+ )}
+
+ );
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/components/ExecutionStartedCard/ExecutionStartedCard.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/components/ExecutionStartedCard/ExecutionStartedCard.tsx
new file mode 100644
index 0000000000..f98656e5ff
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/components/ExecutionStartedCard/ExecutionStartedCard.tsx
@@ -0,0 +1,39 @@
+"use client";
+
+import type { ExecutionStartedResponse } from "@/app/api/__generated__/models/executionStartedResponse";
+import { Button } from "@/components/atoms/Button/Button";
+import { useRouter } from "next/navigation";
+import {
+ ContentCard,
+ ContentCardDescription,
+ ContentCardSubtitle,
+ ContentCardTitle,
+ ContentGrid,
+} from "../../../../components/ToolAccordion/AccordionContent";
+
+interface Props {
+ output: ExecutionStartedResponse;
+}
+
+export function ExecutionStartedCard({ output }: Props) {
+ const router = useRouter();
+
+ return (
+
+
+ Execution started
+ {output.execution_id}
+ {output.message}
+ {output.library_agent_link && (
+
+ )}
+
+
+ );
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/components/SetupRequirementsCard/SetupRequirementsCard.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/components/SetupRequirementsCard/SetupRequirementsCard.tsx
new file mode 100644
index 0000000000..c6d116e62a
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/components/SetupRequirementsCard/SetupRequirementsCard.tsx
@@ -0,0 +1,105 @@
+"use client";
+
+import { useState } from "react";
+import { CredentialsGroupedView } from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/CredentialsGroupedView";
+import { Button } from "@/components/atoms/Button/Button";
+import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
+import type { SetupRequirementsResponse } from "@/app/api/__generated__/models/setupRequirementsResponse";
+import { useCopilotChatActions } from "../../../../components/CopilotChatActionsProvider/useCopilotChatActions";
+import {
+ ContentBadge,
+ ContentCardDescription,
+ ContentCardTitle,
+ ContentMessage,
+} from "../../../../components/ToolAccordion/AccordionContent";
+import { coerceCredentialFields, coerceExpectedInputs } from "./helpers";
+
+interface Props {
+ output: SetupRequirementsResponse;
+}
+
+export function SetupRequirementsCard({ output }: Props) {
+ const { onSend } = useCopilotChatActions();
+
+ const [inputCredentials, setInputCredentials] = useState<
+ Record
+ >({});
+ const [hasSent, setHasSent] = useState(false);
+
+ const { credentialFields, requiredCredentials } = coerceCredentialFields(
+ output.setup_info.user_readiness?.missing_credentials,
+ );
+
+ const expectedInputs = coerceExpectedInputs(
+ (output.setup_info.requirements as Record)?.inputs,
+ );
+
+ function handleCredentialChange(key: string, value?: CredentialsMetaInput) {
+ setInputCredentials((prev) => ({ ...prev, [key]: value }));
+ }
+
+ const isAllComplete =
+ credentialFields.length > 0 &&
+ [...requiredCredentials].every((key) => !!inputCredentials[key]);
+
+ function handleProceed() {
+ setHasSent(true);
+ onSend(
+ "I've configured the required credentials. Please check if everything is ready and proceed with running the agent.",
+ );
+ }
+
+ return (
+
+
{output.message}
+
+ {credentialFields.length > 0 && (
+
+
+ {isAllComplete && !hasSent && (
+
+ )}
+
+ )}
+
+ {expectedInputs.length > 0 && (
+
+
+ Expected inputs
+
+
+ {expectedInputs.map((input) => (
+
+
+
+ {input.title}
+
+
+ {input.required ? "Required" : "Optional"}
+
+
+
+ {input.name} • {input.type}
+ {input.description ? ` \u2022 ${input.description}` : ""}
+
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/components/SetupRequirementsCard/helpers.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/components/SetupRequirementsCard/helpers.ts
new file mode 100644
index 0000000000..6bb10751f0
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/components/SetupRequirementsCard/helpers.ts
@@ -0,0 +1,116 @@
+import type { CredentialField } from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/helpers";
+
+const VALID_CREDENTIAL_TYPES = new Set([
+ "api_key",
+ "oauth2",
+ "user_password",
+ "host_scoped",
+]);
+
+/**
+ * Transforms raw missing_credentials from SetupRequirementsResponse
+ * into CredentialField[] tuples compatible with CredentialsGroupedView.
+ *
+ * Each CredentialField is [key, schema] where schema matches
+ * BlockIOCredentialsSubSchema shape.
+ */
+export function coerceCredentialFields(rawMissingCredentials: unknown): {
+ credentialFields: CredentialField[];
+ requiredCredentials: Set;
+} {
+ const missing =
+ rawMissingCredentials && typeof rawMissingCredentials === "object"
+ ? (rawMissingCredentials as Record)
+ : {};
+
+ const credentialFields: CredentialField[] = [];
+ const requiredCredentials = new Set();
+
+ Object.entries(missing).forEach(([key, value]) => {
+ if (!value || typeof value !== "object") return;
+ const cred = value as Record;
+
+ const provider =
+ typeof cred.provider === "string" ? cred.provider.trim() : "";
+ if (!provider) return;
+
+ const types =
+ Array.isArray(cred.types) && cred.types.length > 0
+ ? cred.types
+ : typeof cred.type === "string"
+ ? [cred.type]
+ : [];
+
+ const credentialTypes = types
+ .map((t) => (typeof t === "string" ? t.trim() : ""))
+ .filter((t) => VALID_CREDENTIAL_TYPES.has(t));
+
+ if (credentialTypes.length === 0) return;
+
+ const scopes = Array.isArray(cred.scopes)
+ ? cred.scopes.filter((s): s is string => typeof s === "string")
+ : undefined;
+
+ const schema = {
+ type: "object" as const,
+ properties: {},
+ credentials_provider: [provider],
+ credentials_types: credentialTypes,
+ credentials_scopes: scopes,
+ };
+
+ credentialFields.push([key, schema]);
+ requiredCredentials.add(key);
+ });
+
+ return { credentialFields, requiredCredentials };
+}
+
+export function coerceExpectedInputs(rawInputs: unknown): Array<{
+ name: string;
+ title: string;
+ type: string;
+ description?: string;
+ required: boolean;
+}> {
+ if (!Array.isArray(rawInputs)) return [];
+ const results: Array<{
+ name: string;
+ title: string;
+ type: string;
+ description?: string;
+ required: boolean;
+ }> = [];
+
+ rawInputs.forEach((value, index) => {
+ if (!value || typeof value !== "object") return;
+ const input = value as Record;
+
+ const name =
+ typeof input.name === "string" && input.name.trim()
+ ? input.name.trim()
+ : `input-${index}`;
+ const title =
+ typeof input.title === "string" && input.title.trim()
+ ? input.title.trim()
+ : name;
+ const type = typeof input.type === "string" ? input.type : "unknown";
+ const description =
+ typeof input.description === "string" && input.description.trim()
+ ? input.description.trim()
+ : undefined;
+ const required = Boolean(input.required);
+
+ const item: {
+ name: string;
+ title: string;
+ type: string;
+ description?: string;
+ required: boolean;
+ } = { name, title, type, required };
+ if (description) item.description = description;
+ results.push(item);
+ });
+
+ return results;
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/helpers.tsx
new file mode 100644
index 0000000000..816c661230
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunAgent/helpers.tsx
@@ -0,0 +1,248 @@
+import type { AgentDetailsResponse } from "@/app/api/__generated__/models/agentDetailsResponse";
+import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse";
+import type { ExecutionStartedResponse } from "@/app/api/__generated__/models/executionStartedResponse";
+import type { NeedLoginResponse } from "@/app/api/__generated__/models/needLoginResponse";
+import { ResponseType } from "@/app/api/__generated__/models/responseType";
+import type { SetupRequirementsResponse } from "@/app/api/__generated__/models/setupRequirementsResponse";
+import {
+ PlayIcon,
+ RocketLaunchIcon,
+ WarningDiamondIcon,
+} from "@phosphor-icons/react";
+import type { ToolUIPart } from "ai";
+import { OrbitLoader } from "../../components/OrbitLoader/OrbitLoader";
+
+export interface RunAgentInput {
+ username_agent_slug?: string;
+ library_agent_id?: string;
+ inputs?: Record;
+ use_defaults?: boolean;
+ schedule_name?: string;
+ cron?: string;
+ timezone?: string;
+}
+
+export type RunAgentToolOutput =
+ | SetupRequirementsResponse
+ | ExecutionStartedResponse
+ | AgentDetailsResponse
+ | NeedLoginResponse
+ | ErrorResponse;
+
+const RUN_AGENT_OUTPUT_TYPES = new Set([
+ ResponseType.setup_requirements,
+ ResponseType.execution_started,
+ ResponseType.agent_details,
+ ResponseType.need_login,
+ ResponseType.error,
+]);
+
+export function isRunAgentSetupRequirementsOutput(
+ output: RunAgentToolOutput,
+): output is SetupRequirementsResponse {
+ return (
+ output.type === ResponseType.setup_requirements ||
+ ("setup_info" in output && typeof output.setup_info === "object")
+ );
+}
+
+export function isRunAgentExecutionStartedOutput(
+ output: RunAgentToolOutput,
+): output is ExecutionStartedResponse {
+ return (
+ output.type === ResponseType.execution_started || "execution_id" in output
+ );
+}
+
+export function isRunAgentAgentDetailsOutput(
+ output: RunAgentToolOutput,
+): output is AgentDetailsResponse {
+ return output.type === ResponseType.agent_details || "agent" in output;
+}
+
+export function isRunAgentNeedLoginOutput(
+ output: RunAgentToolOutput,
+): output is NeedLoginResponse {
+ return output.type === ResponseType.need_login;
+}
+
+export function isRunAgentErrorOutput(
+ output: RunAgentToolOutput,
+): output is ErrorResponse {
+ return output.type === ResponseType.error || "error" in output;
+}
+
+function parseOutput(output: unknown): RunAgentToolOutput | null {
+ if (!output) return null;
+ if (typeof output === "string") {
+ const trimmed = output.trim();
+ if (!trimmed) return null;
+ try {
+ return parseOutput(JSON.parse(trimmed) as unknown);
+ } catch {
+ return null;
+ }
+ }
+ if (typeof output === "object") {
+ const type = (output as { type?: unknown }).type;
+ if (typeof type === "string" && RUN_AGENT_OUTPUT_TYPES.has(type)) {
+ return output as RunAgentToolOutput;
+ }
+ if ("execution_id" in output) return output as ExecutionStartedResponse;
+ if ("setup_info" in output) return output as SetupRequirementsResponse;
+ if ("agent" in output) return output as AgentDetailsResponse;
+ if ("error" in output || "details" in output)
+ return output as ErrorResponse;
+ if (type === ResponseType.need_login) return output as NeedLoginResponse;
+ }
+ return null;
+}
+
+export function getRunAgentToolOutput(
+ part: unknown,
+): RunAgentToolOutput | null {
+ if (!part || typeof part !== "object") return null;
+ return parseOutput((part as { output?: unknown }).output);
+}
+
+function getAgentIdentifierText(
+ input: RunAgentInput | undefined,
+): string | null {
+ if (!input) return null;
+ const slug = input.username_agent_slug?.trim();
+ if (slug) return slug;
+ const libraryId = input.library_agent_id?.trim();
+ if (libraryId) return `Library agent ${libraryId}`;
+ return null;
+}
+
+export function getAnimationText(part: {
+ state: ToolUIPart["state"];
+ input?: unknown;
+ output?: unknown;
+}): string {
+ const input = part.input as RunAgentInput | undefined;
+ const agentIdentifier = getAgentIdentifierText(input);
+ const isSchedule = Boolean(
+ input?.schedule_name?.trim() || input?.cron?.trim(),
+ );
+ const actionPhrase = isSchedule
+ ? "Scheduling the agent to run"
+ : "Running the agent";
+ const identifierText = agentIdentifier ? ` "${agentIdentifier}"` : "";
+
+ switch (part.state) {
+ case "input-streaming":
+ case "input-available":
+ return `${actionPhrase}${identifierText}`;
+ case "output-available": {
+ const output = parseOutput(part.output);
+ if (!output) return `${actionPhrase}${identifierText}`;
+ if (isRunAgentExecutionStartedOutput(output)) {
+ return `Started "${output.graph_name}"`;
+ }
+ if (isRunAgentAgentDetailsOutput(output)) {
+ return `Agent inputs needed for "${output.agent.name}"`;
+ }
+ if (isRunAgentSetupRequirementsOutput(output)) {
+ return `Setup needed for "${output.setup_info.agent_name}"`;
+ }
+ if (isRunAgentNeedLoginOutput(output))
+ return "Sign in required to run agent";
+ return "Error running agent";
+ }
+ case "output-error":
+ return "Error running agent";
+ default:
+ return actionPhrase;
+ }
+}
+
+export function ToolIcon({
+ isStreaming,
+ isError,
+}: {
+ isStreaming?: boolean;
+ isError?: boolean;
+}) {
+ if (isError) {
+ return (
+
+ );
+ }
+ if (isStreaming) {
+ return ;
+ }
+ return ;
+}
+
+export function AccordionIcon() {
+ return ;
+}
+
+export function formatMaybeJson(value: unknown): string {
+ if (typeof value === "string") return value;
+ try {
+ return JSON.stringify(value, null, 2);
+ } catch {
+ return String(value);
+ }
+}
+
+export function getAccordionMeta(output: RunAgentToolOutput): {
+ icon: React.ReactNode;
+ title: string;
+ titleClassName?: string;
+ description?: string;
+} {
+ const icon = ;
+
+ if (isRunAgentExecutionStartedOutput(output)) {
+ const statusText =
+ typeof output.status === "string" && output.status.trim()
+ ? output.status.trim()
+ : "started";
+ return {
+ icon: ,
+ title: output.graph_name,
+ description: `Status: ${statusText}`,
+ };
+ }
+
+ if (isRunAgentAgentDetailsOutput(output)) {
+ return {
+ icon,
+ title: output.agent.name,
+ description: "Inputs required",
+ };
+ }
+
+ if (isRunAgentSetupRequirementsOutput(output)) {
+ const missingCredsCount = Object.keys(
+ (output.setup_info.user_readiness?.missing_credentials ?? {}) as Record<
+ string,
+ unknown
+ >,
+ ).length;
+ return {
+ icon,
+ title: output.setup_info.agent_name,
+ description:
+ missingCredsCount > 0
+ ? `Missing ${missingCredsCount} credential${missingCredsCount === 1 ? "" : "s"}`
+ : output.message,
+ };
+ }
+
+ if (isRunAgentNeedLoginOutput(output)) {
+ return { icon, title: "Sign in required" };
+ }
+
+ return {
+ icon: (
+
+ ),
+ title: "Error",
+ titleClassName: "text-red-500",
+ };
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/RunBlock.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/RunBlock.tsx
new file mode 100644
index 0000000000..e1cb030449
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/RunBlock.tsx
@@ -0,0 +1,70 @@
+"use client";
+
+import type { ToolUIPart } from "ai";
+import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
+import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
+import { BlockOutputCard } from "./components/BlockOutputCard/BlockOutputCard";
+import { ErrorCard } from "./components/ErrorCard/ErrorCard";
+import { SetupRequirementsCard } from "./components/SetupRequirementsCard/SetupRequirementsCard";
+import {
+ getAccordionMeta,
+ getAnimationText,
+ getRunBlockToolOutput,
+ isRunBlockBlockOutput,
+ isRunBlockErrorOutput,
+ isRunBlockSetupRequirementsOutput,
+ ToolIcon,
+} from "./helpers";
+
+export interface RunBlockToolPart {
+ type: string;
+ toolCallId: string;
+ state: ToolUIPart["state"];
+ input?: unknown;
+ output?: unknown;
+}
+
+interface Props {
+ part: RunBlockToolPart;
+}
+
+export function RunBlockTool({ part }: Props) {
+ const text = getAnimationText(part);
+ const isStreaming =
+ part.state === "input-streaming" || part.state === "input-available";
+
+ const output = getRunBlockToolOutput(part);
+ const isError =
+ part.state === "output-error" ||
+ (!!output && isRunBlockErrorOutput(output));
+ const hasExpandableContent =
+ part.state === "output-available" &&
+ !!output &&
+ (isRunBlockBlockOutput(output) ||
+ isRunBlockSetupRequirementsOutput(output) ||
+ isRunBlockErrorOutput(output));
+
+ return (
+
+
+
+
+
+
+ {hasExpandableContent && output && (
+
+ {isRunBlockBlockOutput(output) && }
+
+ {isRunBlockSetupRequirementsOutput(output) && (
+
+ )}
+
+ {isRunBlockErrorOutput(output) && }
+
+ )}
+
+ );
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/components/BlockOutputCard/BlockOutputCard.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/components/BlockOutputCard/BlockOutputCard.tsx
new file mode 100644
index 0000000000..4051927653
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/components/BlockOutputCard/BlockOutputCard.tsx
@@ -0,0 +1,133 @@
+"use client";
+
+import React, { useState } from "react";
+import { getGetWorkspaceDownloadFileByIdUrl } from "@/app/api/__generated__/endpoints/workspace/workspace";
+import { Button } from "@/components/atoms/Button/Button";
+import type { BlockOutputResponse } from "@/app/api/__generated__/models/blockOutputResponse";
+import {
+ globalRegistry,
+ OutputItem,
+} from "@/components/contextual/OutputRenderers";
+import type { OutputMetadata } from "@/components/contextual/OutputRenderers";
+import {
+ ContentBadge,
+ ContentCard,
+ ContentCardTitle,
+ ContentGrid,
+ ContentMessage,
+} from "../../../../components/ToolAccordion/AccordionContent";
+
+interface Props {
+ output: BlockOutputResponse;
+}
+
+const COLLAPSED_LIMIT = 3;
+
+function isWorkspaceRef(value: unknown): value is string {
+ return typeof value === "string" && value.startsWith("workspace://");
+}
+
+function resolveForRenderer(value: unknown): {
+ value: unknown;
+ metadata?: OutputMetadata;
+} {
+ if (!isWorkspaceRef(value)) return { value };
+
+ const withoutPrefix = value.replace("workspace://", "");
+ const fileId = withoutPrefix.split("#")[0];
+ const apiPath = getGetWorkspaceDownloadFileByIdUrl(fileId);
+ const url = `/api/proxy${apiPath}`;
+
+ const hashIndex = value.indexOf("#");
+ const mimeHint =
+ hashIndex !== -1 ? value.slice(hashIndex + 1) || undefined : undefined;
+
+ const metadata: OutputMetadata = {};
+ if (mimeHint) {
+ metadata.mimeType = mimeHint;
+ if (mimeHint.startsWith("image/")) metadata.type = "image";
+ else if (mimeHint.startsWith("video/")) metadata.type = "video";
+ }
+
+ return { value: url, metadata };
+}
+
+function RenderOutputValue({ value }: { value: unknown }) {
+ const resolved = resolveForRenderer(value);
+ const renderer = globalRegistry.getRenderer(
+ resolved.value,
+ resolved.metadata,
+ );
+
+ if (renderer) {
+ return (
+
+ );
+ }
+
+ // Fallback for audio workspace refs
+ if (
+ isWorkspaceRef(value) &&
+ resolved.metadata?.mimeType?.startsWith("audio/")
+ ) {
+ return (
+
+ );
+ }
+
+ return null;
+}
+
+function OutputKeySection({
+ outputKey,
+ items,
+}: {
+ outputKey: string;
+ items: unknown[];
+}) {
+ const [expanded, setExpanded] = useState(false);
+ const hasMoreItems = items.length > COLLAPSED_LIMIT;
+ const visibleItems = expanded ? items : items.slice(0, COLLAPSED_LIMIT);
+
+ return (
+
+
+ {outputKey}
+
+ {items.length} item{items.length === 1 ? "" : "s"}
+
+
+
+ {visibleItems.map((item, i) => (
+
+ ))}
+
+ {hasMoreItems && (
+
+ )}
+
+ );
+}
+
+export function BlockOutputCard({ output }: Props) {
+ return (
+
+ {output.message}
+
+ {Object.entries(output.outputs ?? {}).map(([key, items]) => (
+
+ ))}
+
+ );
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/components/ErrorCard/ErrorCard.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/components/ErrorCard/ErrorCard.tsx
new file mode 100644
index 0000000000..7990428947
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/components/ErrorCard/ErrorCard.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse";
+import {
+ ContentCodeBlock,
+ ContentGrid,
+ ContentMessage,
+} from "../../../../components/ToolAccordion/AccordionContent";
+import { formatMaybeJson } from "../../helpers";
+
+interface Props {
+ output: ErrorResponse;
+}
+
+export function ErrorCard({ output }: Props) {
+ return (
+
+ {output.message}
+ {output.error && (
+ {formatMaybeJson(output.error)}
+ )}
+ {output.details && (
+ {formatMaybeJson(output.details)}
+ )}
+
+ );
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/components/SetupRequirementsCard/SetupRequirementsCard.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/components/SetupRequirementsCard/SetupRequirementsCard.tsx
new file mode 100644
index 0000000000..e98fea2850
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/components/SetupRequirementsCard/SetupRequirementsCard.tsx
@@ -0,0 +1,197 @@
+"use client";
+
+import type { SetupRequirementsResponse } from "@/app/api/__generated__/models/setupRequirementsResponse";
+import { Button } from "@/components/atoms/Button/Button";
+import { Text } from "@/components/atoms/Text/Text";
+import { CredentialsGroupedView } from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/CredentialsGroupedView";
+import { FormRenderer } from "@/components/renderers/InputRenderer/FormRenderer";
+import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
+import { AnimatePresence, motion } from "framer-motion";
+import { useState } from "react";
+import { useCopilotChatActions } from "../../../../components/CopilotChatActionsProvider/useCopilotChatActions";
+import {
+ ContentBadge,
+ ContentCardDescription,
+ ContentCardTitle,
+ ContentMessage,
+} from "../../../../components/ToolAccordion/AccordionContent";
+import {
+ buildExpectedInputsSchema,
+ coerceCredentialFields,
+ coerceExpectedInputs,
+} from "./helpers";
+
+interface Props {
+ output: SetupRequirementsResponse;
+}
+
+export function SetupRequirementsCard({ output }: Props) {
+ const { onSend } = useCopilotChatActions();
+
+ const [inputCredentials, setInputCredentials] = useState<
+ Record
+ >({});
+ const [hasSentCredentials, setHasSentCredentials] = useState(false);
+
+ const [showInputForm, setShowInputForm] = useState(false);
+ const [inputValues, setInputValues] = useState>({});
+
+ const { credentialFields, requiredCredentials } = coerceCredentialFields(
+ output.setup_info.user_readiness?.missing_credentials,
+ );
+
+ const expectedInputs = coerceExpectedInputs(
+ (output.setup_info.requirements as Record)?.inputs,
+ );
+
+ const inputSchema = buildExpectedInputsSchema(expectedInputs);
+
+ function handleCredentialChange(key: string, value?: CredentialsMetaInput) {
+ setInputCredentials((prev) => ({ ...prev, [key]: value }));
+ }
+
+ const isAllCredentialsComplete =
+ credentialFields.length > 0 &&
+ [...requiredCredentials].every((key) => !!inputCredentials[key]);
+
+ function handleProceedCredentials() {
+ setHasSentCredentials(true);
+ onSend(
+ "I've configured the required credentials. Please re-run the block now.",
+ );
+ }
+
+ function handleRunWithInputs() {
+ const nonEmpty = Object.fromEntries(
+ Object.entries(inputValues).filter(
+ ([, v]) => v !== undefined && v !== null && v !== "",
+ ),
+ );
+ onSend(
+ `Run the block with these inputs: ${JSON.stringify(nonEmpty, null, 2)}`,
+ );
+ setShowInputForm(false);
+ setInputValues({});
+ }
+
+ return (
+
+
{output.message}
+
+ {credentialFields.length > 0 && (
+
+
+ {isAllCredentialsComplete && !hasSentCredentials && (
+
+ )}
+
+ )}
+
+ {inputSchema && (
+
+
+
+ )}
+
+
+ {showInputForm && inputSchema && (
+
+
+
Block inputs
+
setInputValues(v.formData ?? {})}
+ uiSchema={{
+ "ui:submitButtonOptions": { norender: true },
+ }}
+ initialValues={inputValues}
+ formContext={{
+ showHandles: false,
+ size: "small",
+ }}
+ />
+
+
+
+
+
+
+ )}
+
+
+ {expectedInputs.length > 0 && !inputSchema && (
+
+
+ Expected inputs
+
+
+ {expectedInputs.map((input) => (
+
+
+
+ {input.title}
+
+
+ {input.required ? "Required" : "Optional"}
+
+
+
+ {input.name} • {input.type}
+ {input.description ? ` \u2022 ${input.description}` : ""}
+
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/components/SetupRequirementsCard/helpers.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/components/SetupRequirementsCard/helpers.ts
new file mode 100644
index 0000000000..bacc0bac55
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/components/SetupRequirementsCard/helpers.ts
@@ -0,0 +1,156 @@
+import type { CredentialField } from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/helpers";
+import type { RJSFSchema } from "@rjsf/utils";
+
+const VALID_CREDENTIAL_TYPES = new Set([
+ "api_key",
+ "oauth2",
+ "user_password",
+ "host_scoped",
+]);
+
+export function coerceCredentialFields(rawMissingCredentials: unknown): {
+ credentialFields: CredentialField[];
+ requiredCredentials: Set;
+} {
+ const missing =
+ rawMissingCredentials && typeof rawMissingCredentials === "object"
+ ? (rawMissingCredentials as Record)
+ : {};
+
+ const credentialFields: CredentialField[] = [];
+ const requiredCredentials = new Set();
+
+ Object.entries(missing).forEach(([key, value]) => {
+ if (!value || typeof value !== "object") return;
+ const cred = value as Record;
+
+ const provider =
+ typeof cred.provider === "string" ? cred.provider.trim() : "";
+ if (!provider) return;
+
+ const types =
+ Array.isArray(cred.types) && cred.types.length > 0
+ ? cred.types
+ : typeof cred.type === "string"
+ ? [cred.type]
+ : [];
+
+ const credentialTypes = types
+ .map((t) => (typeof t === "string" ? t.trim() : ""))
+ .filter((t) => VALID_CREDENTIAL_TYPES.has(t));
+
+ if (credentialTypes.length === 0) return;
+
+ const scopes = Array.isArray(cred.scopes)
+ ? cred.scopes.filter((s): s is string => typeof s === "string")
+ : undefined;
+
+ const schema = {
+ type: "object" as const,
+ properties: {},
+ credentials_provider: [provider],
+ credentials_types: credentialTypes,
+ credentials_scopes: scopes,
+ };
+
+ credentialFields.push([key, schema]);
+ requiredCredentials.add(key);
+ });
+
+ return { credentialFields, requiredCredentials };
+}
+
+export function coerceExpectedInputs(rawInputs: unknown): Array<{
+ name: string;
+ title: string;
+ type: string;
+ description?: string;
+ required: boolean;
+}> {
+ if (!Array.isArray(rawInputs)) return [];
+ const results: Array<{
+ name: string;
+ title: string;
+ type: string;
+ description?: string;
+ required: boolean;
+ }> = [];
+
+ rawInputs.forEach((value, index) => {
+ if (!value || typeof value !== "object") return;
+ const input = value as Record;
+
+ const name =
+ typeof input.name === "string" && input.name.trim()
+ ? input.name.trim()
+ : `input-${index}`;
+ const title =
+ typeof input.title === "string" && input.title.trim()
+ ? input.title.trim()
+ : name;
+ const type = typeof input.type === "string" ? input.type : "unknown";
+ const description =
+ typeof input.description === "string" && input.description.trim()
+ ? input.description.trim()
+ : undefined;
+ const required = Boolean(input.required);
+
+ const item: {
+ name: string;
+ title: string;
+ type: string;
+ description?: string;
+ required: boolean;
+ } = { name, title, type, required };
+ if (description) item.description = description;
+ results.push(item);
+ });
+
+ return results;
+}
+
+/**
+ * Build an RJSF schema from expected inputs so they can be rendered
+ * as a dynamic form via FormRenderer.
+ */
+export function buildExpectedInputsSchema(
+ expectedInputs: Array<{
+ name: string;
+ title: string;
+ type: string;
+ description?: string;
+ required: boolean;
+ }>,
+): RJSFSchema | null {
+ if (expectedInputs.length === 0) return null;
+
+ const TYPE_MAP: Record = {
+ string: "string",
+ str: "string",
+ text: "string",
+ number: "number",
+ int: "integer",
+ integer: "integer",
+ float: "number",
+ boolean: "boolean",
+ bool: "boolean",
+ };
+
+ const properties: Record> = {};
+ const required: string[] = [];
+
+ for (const input of expectedInputs) {
+ properties[input.name] = {
+ type: TYPE_MAP[input.type.toLowerCase()] ?? "string",
+ title: input.title,
+ ...(input.description ? { description: input.description } : {}),
+ };
+ if (input.required) required.push(input.name);
+ }
+
+ return {
+ type: "object",
+ properties,
+ ...(required.length > 0 ? { required } : {}),
+ };
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/helpers.tsx
new file mode 100644
index 0000000000..c9b903876a
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/RunBlock/helpers.tsx
@@ -0,0 +1,185 @@
+import type { BlockOutputResponse } from "@/app/api/__generated__/models/blockOutputResponse";
+import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse";
+import { ResponseType } from "@/app/api/__generated__/models/responseType";
+import type { SetupRequirementsResponse } from "@/app/api/__generated__/models/setupRequirementsResponse";
+import {
+ PlayCircleIcon,
+ PlayIcon,
+ WarningDiamondIcon,
+} from "@phosphor-icons/react";
+import type { ToolUIPart } from "ai";
+import { OrbitLoader } from "../../components/OrbitLoader/OrbitLoader";
+
+export interface RunBlockInput {
+ block_id?: string;
+ input_data?: Record;
+}
+
+export type RunBlockToolOutput =
+ | SetupRequirementsResponse
+ | BlockOutputResponse
+ | ErrorResponse;
+
+const RUN_BLOCK_OUTPUT_TYPES = new Set([
+ ResponseType.setup_requirements,
+ ResponseType.block_output,
+ ResponseType.error,
+]);
+
+export function isRunBlockSetupRequirementsOutput(
+ output: RunBlockToolOutput,
+): output is SetupRequirementsResponse {
+ return (
+ output.type === ResponseType.setup_requirements ||
+ ("setup_info" in output && typeof output.setup_info === "object")
+ );
+}
+
+export function isRunBlockBlockOutput(
+ output: RunBlockToolOutput,
+): output is BlockOutputResponse {
+ return output.type === ResponseType.block_output || "block_id" in output;
+}
+
+export function isRunBlockErrorOutput(
+ output: RunBlockToolOutput,
+): output is ErrorResponse {
+ return output.type === ResponseType.error || "error" in output;
+}
+
+function parseOutput(output: unknown): RunBlockToolOutput | null {
+ if (!output) return null;
+ if (typeof output === "string") {
+ const trimmed = output.trim();
+ if (!trimmed) return null;
+ try {
+ return parseOutput(JSON.parse(trimmed) as unknown);
+ } catch {
+ return null;
+ }
+ }
+ if (typeof output === "object") {
+ const type = (output as { type?: unknown }).type;
+ if (typeof type === "string" && RUN_BLOCK_OUTPUT_TYPES.has(type)) {
+ return output as RunBlockToolOutput;
+ }
+ if ("block_id" in output) return output as BlockOutputResponse;
+ if ("setup_info" in output) return output as SetupRequirementsResponse;
+ if ("error" in output || "details" in output)
+ return output as ErrorResponse;
+ }
+ return null;
+}
+
+export function getRunBlockToolOutput(
+ part: unknown,
+): RunBlockToolOutput | null {
+ if (!part || typeof part !== "object") return null;
+ return parseOutput((part as { output?: unknown }).output);
+}
+
+export function getAnimationText(part: {
+ state: ToolUIPart["state"];
+ input?: unknown;
+ output?: unknown;
+}): string {
+ const input = part.input as RunBlockInput | undefined;
+ const blockId = input?.block_id?.trim();
+ const blockText = blockId ? ` "${blockId}"` : "";
+
+ switch (part.state) {
+ case "input-streaming":
+ case "input-available":
+ return `Running the block${blockText}`;
+ case "output-available": {
+ const output = parseOutput(part.output);
+ if (!output) return `Running the block${blockText}`;
+ if (isRunBlockBlockOutput(output)) return `Ran "${output.block_name}"`;
+ if (isRunBlockSetupRequirementsOutput(output)) {
+ return `Setup needed for "${output.setup_info.agent_name}"`;
+ }
+ return "Error running block";
+ }
+ case "output-error":
+ return "Error running block";
+ default:
+ return "Running the block";
+ }
+}
+
+export function ToolIcon({
+ isStreaming,
+ isError,
+}: {
+ isStreaming?: boolean;
+ isError?: boolean;
+}) {
+ if (isError) {
+ return (
+
+ );
+ }
+ if (isStreaming) {
+ return ;
+ }
+ return ;
+}
+
+export function AccordionIcon() {
+ return ;
+}
+
+export function formatMaybeJson(value: unknown): string {
+ if (typeof value === "string") return value;
+ try {
+ return JSON.stringify(value, null, 2);
+ } catch {
+ return String(value);
+ }
+}
+
+export function getAccordionMeta(output: RunBlockToolOutput): {
+ icon: React.ReactNode;
+ title: string;
+ titleClassName?: string;
+ description?: string;
+} {
+ const icon = ;
+
+ if (isRunBlockBlockOutput(output)) {
+ const keys = Object.keys(output.outputs ?? {});
+ return {
+ icon: ,
+ title: output.block_name,
+ description:
+ keys.length > 0
+ ? `${keys.length} output key${keys.length === 1 ? "" : "s"}`
+ : output.message,
+ };
+ }
+
+ if (isRunBlockSetupRequirementsOutput(output)) {
+ const missingCredsCount = Object.keys(
+ (output.setup_info.user_readiness?.missing_credentials ?? {}) as Record<
+ string,
+ unknown
+ >,
+ ).length;
+ return {
+ icon,
+ title: output.setup_info.agent_name,
+ description:
+ missingCredsCount > 0
+ ? `Missing ${missingCredsCount} credential${missingCredsCount === 1 ? "" : "s"}`
+ : output.message,
+ };
+ }
+
+ return {
+ icon: (
+
+ ),
+ title: "Error",
+ titleClassName: "text-red-500",
+ };
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/SearchDocs/SearchDocs.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/SearchDocs/SearchDocs.tsx
new file mode 100644
index 0000000000..d3fb793d14
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/SearchDocs/SearchDocs.tsx
@@ -0,0 +1,186 @@
+"use client";
+
+import type { ToolUIPart } from "ai";
+import { useMemo } from "react";
+
+import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
+import {
+ ContentCard,
+ ContentCardDescription,
+ ContentCardHeader,
+ ContentCardSubtitle,
+ ContentCardTitle,
+ ContentGrid,
+ ContentLink,
+ ContentMessage,
+ ContentSuggestionsList,
+} from "../../components/ToolAccordion/AccordionContent";
+import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
+import {
+ AccordionIcon,
+ getAnimationText,
+ getDocsToolOutput,
+ getDocsToolTitle,
+ getToolLabel,
+ isDocPageOutput,
+ isDocSearchResultsOutput,
+ isErrorOutput,
+ isNoResultsOutput,
+ toDocsUrl,
+ ToolIcon,
+ type DocsToolType,
+} from "./helpers";
+
+export interface DocsToolPart {
+ type: DocsToolType;
+ toolCallId: string;
+ state: ToolUIPart["state"];
+ input?: unknown;
+ output?: unknown;
+}
+
+interface Props {
+ part: DocsToolPart;
+}
+
+function truncate(text: string, maxChars: number): string {
+ const trimmed = text.trim();
+ if (trimmed.length <= maxChars) return trimmed;
+ return `${trimmed.slice(0, maxChars).trimEnd()}…`;
+}
+
+export function SearchDocsTool({ part }: Props) {
+ const output = getDocsToolOutput(part);
+ const text = getAnimationText(part);
+ const isStreaming =
+ part.state === "input-streaming" || part.state === "input-available";
+ const isError =
+ part.state === "output-error" || (!!output && isErrorOutput(output));
+
+ const normalized = useMemo(() => {
+ if (!output) return null;
+ const title = getDocsToolTitle(part.type, output);
+ const label = getToolLabel(part.type);
+ return { title, label };
+ }, [output, part.type]);
+
+ const isOutputAvailable = part.state === "output-available" && !!output;
+
+ const docSearchOutput =
+ isOutputAvailable && output && isDocSearchResultsOutput(output)
+ ? output
+ : null;
+ const docPageOutput =
+ isOutputAvailable && output && isDocPageOutput(output) ? output : null;
+ const noResultsOutput =
+ isOutputAvailable && output && isNoResultsOutput(output) ? output : null;
+ const errorOutput =
+ isOutputAvailable && output && isErrorOutput(output) ? output : null;
+
+ const hasExpandableContent =
+ isOutputAvailable &&
+ ((!!docSearchOutput && docSearchOutput.count > 0) ||
+ !!docPageOutput ||
+ !!noResultsOutput ||
+ !!errorOutput);
+
+ const accordionDescription =
+ hasExpandableContent && docSearchOutput
+ ? `Found ${docSearchOutput.count} result${docSearchOutput.count === 1 ? "" : "s"} for "${docSearchOutput.query}"`
+ : hasExpandableContent && docPageOutput
+ ? docPageOutput.path
+ : hasExpandableContent && (noResultsOutput || errorOutput)
+ ? ((noResultsOutput ?? errorOutput)?.message ?? null)
+ : null;
+
+ return (
+
+
+
+
+
+
+ {hasExpandableContent && normalized && (
+
}
+ title={normalized.title}
+ description={accordionDescription}
+ >
+ {docSearchOutput && (
+
+ {docSearchOutput.results.map((r) => {
+ const href = r.doc_url ?? toDocsUrl(r.path);
+ return (
+
+ Open}
+ >
+ {r.title}
+
+ {r.path}
+ {r.section ? ` • ${r.section}` : ""}
+
+
+ {truncate(r.snippet, 240)}
+
+
+
+ );
+ })}
+
+ )}
+
+ {docPageOutput && (
+
+
+ Open
+
+ }
+ >
+ {docPageOutput.title}
+ {docPageOutput.path}
+
+
+ {truncate(docPageOutput.content, 800)}
+
+
+ )}
+
+ {noResultsOutput && (
+
+ {noResultsOutput.message}
+ {noResultsOutput.suggestions &&
+ noResultsOutput.suggestions.length > 0 && (
+
+ )}
+
+ )}
+
+ {errorOutput && (
+
+ {errorOutput.message}
+ {errorOutput.error && (
+
+ {errorOutput.error}
+
+ )}
+
+ )}
+
+ )}
+
+ );
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/SearchDocs/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/SearchDocs/helpers.tsx
new file mode 100644
index 0000000000..50a84edceb
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/SearchDocs/helpers.tsx
@@ -0,0 +1,215 @@
+import type { DocPageResponse } from "@/app/api/__generated__/models/docPageResponse";
+import type { DocSearchResultsResponse } from "@/app/api/__generated__/models/docSearchResultsResponse";
+import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse";
+import type { NoResultsResponse } from "@/app/api/__generated__/models/noResultsResponse";
+import { ResponseType } from "@/app/api/__generated__/models/responseType";
+import {
+ ArticleIcon,
+ FileMagnifyingGlassIcon,
+ FileTextIcon,
+} from "@phosphor-icons/react";
+import { ToolUIPart } from "ai";
+
+export interface SearchDocsInput {
+ query: string;
+}
+
+export interface GetDocPageInput {
+ path: string;
+}
+
+export type DocsToolOutput =
+ | DocSearchResultsResponse
+ | DocPageResponse
+ | NoResultsResponse
+ | ErrorResponse;
+
+export type DocsToolType = "tool-search_docs" | "tool-get_doc_page" | string;
+
+export function getToolLabel(toolType: DocsToolType): string {
+ switch (toolType) {
+ case "tool-search_docs":
+ return "Docs";
+ case "tool-get_doc_page":
+ return "Docs page";
+ default:
+ return "Docs";
+ }
+}
+
+function parseOutput(output: unknown): DocsToolOutput | null {
+ if (!output) return null;
+ if (typeof output === "string") {
+ const trimmed = output.trim();
+ if (!trimmed) return null;
+ try {
+ return parseOutput(JSON.parse(trimmed) as unknown);
+ } catch {
+ return null;
+ }
+ }
+ if (typeof output === "object") {
+ const type = (output as { type?: unknown }).type;
+ if (
+ type === ResponseType.doc_search_results ||
+ type === ResponseType.doc_page ||
+ type === ResponseType.no_results ||
+ type === ResponseType.error
+ ) {
+ return output as DocsToolOutput;
+ }
+ if ("results" in output && "query" in output)
+ return output as DocSearchResultsResponse;
+ if ("content" in output && "path" in output)
+ return output as DocPageResponse;
+ if ("suggestions" in output && !("error" in output))
+ return output as NoResultsResponse;
+ if ("error" in output || "details" in output)
+ return output as ErrorResponse;
+ }
+ return null;
+}
+
+export function getDocsToolOutput(part: unknown): DocsToolOutput | null {
+ if (!part || typeof part !== "object") return null;
+ return parseOutput((part as { output?: unknown }).output);
+}
+
+export function isDocSearchResultsOutput(
+ output: DocsToolOutput,
+): output is DocSearchResultsResponse {
+ return output.type === ResponseType.doc_search_results || "results" in output;
+}
+
+export function isDocPageOutput(
+ output: DocsToolOutput,
+): output is DocPageResponse {
+ return output.type === ResponseType.doc_page || "content" in output;
+}
+
+export function isNoResultsOutput(
+ output: DocsToolOutput,
+): output is NoResultsResponse {
+ return (
+ output.type === ResponseType.no_results ||
+ ("suggestions" in output && !("error" in output))
+ );
+}
+
+export function isErrorOutput(output: DocsToolOutput): output is ErrorResponse {
+ return output.type === ResponseType.error || "error" in output;
+}
+
+export function getDocsToolTitle(
+ toolType: DocsToolType,
+ output: DocsToolOutput,
+): string {
+ if (toolType === "tool-search_docs") {
+ if (isDocSearchResultsOutput(output)) return "Documentation results";
+ if (isNoResultsOutput(output)) return "No documentation found";
+ return "Documentation search error";
+ }
+
+ if (isDocPageOutput(output)) return "Documentation page";
+ if (isNoResultsOutput(output)) return "No documentation found";
+ return "Documentation page error";
+}
+
+export function getAnimationText(part: {
+ type: DocsToolType;
+ state: ToolUIPart["state"];
+ input?: unknown;
+ output?: unknown;
+}): string {
+ switch (part.type) {
+ case "tool-search_docs": {
+ const query = (part.input as SearchDocsInput | undefined)?.query?.trim();
+ const queryText = query ? ` for "${query}"` : "";
+
+ switch (part.state) {
+ case "input-streaming":
+ case "input-available":
+ return `Searching documentation${queryText}`;
+ case "output-available": {
+ const output = parseOutput(part.output);
+ if (!output) return `Searching documentation${queryText}`;
+ if (isDocSearchResultsOutput(output)) {
+ const count = output.count ?? output.results.length;
+ return `Found ${count} result${count === 1 ? "" : "s"}${queryText}`;
+ }
+ if (isNoResultsOutput(output)) {
+ return `No results found${queryText}`;
+ }
+ return `Error searching documentation${queryText}`;
+ }
+ case "output-error":
+ return `Error searching documentation${queryText}`;
+ default:
+ return "Searching documentation";
+ }
+ }
+ case "tool-get_doc_page": {
+ const path = (part.input as GetDocPageInput | undefined)?.path?.trim();
+ const pathText = path ? ` "${path}"` : "";
+
+ switch (part.state) {
+ case "input-streaming":
+ case "input-available":
+ return `Loading documentation page${pathText}`;
+ case "output-available": {
+ const output = parseOutput(part.output);
+ if (!output) return `Loading documentation page${pathText}`;
+ if (isDocPageOutput(output)) return `Loaded "${output.title}"`;
+ if (isNoResultsOutput(output)) return "Documentation page not found";
+ return "Error loading documentation page";
+ }
+ case "output-error":
+ return "Error loading documentation page";
+ default:
+ return "Loading documentation page";
+ }
+ }
+ }
+
+ return "Processing";
+}
+
+export function ToolIcon({
+ toolType,
+ isStreaming,
+ isError,
+}: {
+ toolType: DocsToolType;
+ isStreaming?: boolean;
+ isError?: boolean;
+}) {
+ const IconComponent =
+ toolType === "tool-get_doc_page" ? FileTextIcon : FileMagnifyingGlassIcon;
+
+ return (
+
+ );
+}
+
+export function AccordionIcon({ toolType }: { toolType: DocsToolType }) {
+ const IconComponent =
+ toolType === "tool-get_doc_page" ? ArticleIcon : FileTextIcon;
+ return ;
+}
+
+export function toDocsUrl(path: string): string {
+ const urlPath = path.includes(".")
+ ? path.slice(0, path.lastIndexOf("."))
+ : path;
+ return `https://docs.agpt.co/${urlPath}`;
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/ViewAgentOutput/ViewAgentOutput.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/ViewAgentOutput/ViewAgentOutput.tsx
new file mode 100644
index 0000000000..e7265ac7cb
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/ViewAgentOutput/ViewAgentOutput.tsx
@@ -0,0 +1,261 @@
+"use client";
+
+import type { ToolUIPart } from "ai";
+import React from "react";
+import { getGetWorkspaceDownloadFileByIdUrl } from "@/app/api/__generated__/endpoints/workspace/workspace";
+import {
+ globalRegistry,
+ OutputItem,
+} from "@/components/contextual/OutputRenderers";
+import type { OutputMetadata } from "@/components/contextual/OutputRenderers";
+import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
+import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
+import {
+ ContentBadge,
+ ContentCard,
+ ContentCardHeader,
+ ContentCardSubtitle,
+ ContentCardTitle,
+ ContentCodeBlock,
+ ContentGrid,
+ ContentLink,
+ ContentMessage,
+ ContentSuggestionsList,
+} from "../../components/ToolAccordion/AccordionContent";
+import {
+ formatMaybeJson,
+ getAnimationText,
+ getViewAgentOutputToolOutput,
+ isAgentOutputResponse,
+ isErrorResponse,
+ isNoResultsResponse,
+ AccordionIcon,
+ ToolIcon,
+ type ViewAgentOutputToolOutput,
+} from "./helpers";
+
+export interface ViewAgentOutputToolPart {
+ type: string;
+ toolCallId: string;
+ state: ToolUIPart["state"];
+ input?: unknown;
+ output?: unknown;
+}
+
+interface Props {
+ part: ViewAgentOutputToolPart;
+}
+
+function isWorkspaceRef(value: unknown): value is string {
+ return typeof value === "string" && value.startsWith("workspace://");
+}
+
+function resolveForRenderer(value: unknown): {
+ value: unknown;
+ metadata?: OutputMetadata;
+} {
+ if (!isWorkspaceRef(value)) return { value };
+
+ const withoutPrefix = value.replace("workspace://", "");
+ const fileId = withoutPrefix.split("#")[0];
+ const apiPath = getGetWorkspaceDownloadFileByIdUrl(fileId);
+ const url = `/api/proxy${apiPath}`;
+
+ const hashIndex = value.indexOf("#");
+ const mimeHint =
+ hashIndex !== -1 ? value.slice(hashIndex + 1) || undefined : undefined;
+
+ const metadata: OutputMetadata = {};
+ if (mimeHint) {
+ metadata.mimeType = mimeHint;
+ if (mimeHint.startsWith("image/")) metadata.type = "image";
+ else if (mimeHint.startsWith("video/")) metadata.type = "video";
+ }
+
+ return { value: url, metadata };
+}
+
+function RenderOutputValue({ value }: { value: unknown }) {
+ const resolved = resolveForRenderer(value);
+ const renderer = globalRegistry.getRenderer(
+ resolved.value,
+ resolved.metadata,
+ );
+
+ if (renderer) {
+ return (
+
+ );
+ }
+
+ // Fallback for audio workspace refs
+ if (
+ isWorkspaceRef(value) &&
+ resolved.metadata?.mimeType?.startsWith("audio/")
+ ) {
+ return (
+
+ );
+ }
+
+ return null;
+}
+
+function getAccordionMeta(output: ViewAgentOutputToolOutput): {
+ icon: React.ReactNode;
+ title: string;
+ description?: string;
+} {
+ const icon = ;
+
+ if (isAgentOutputResponse(output)) {
+ const status = output.execution?.status;
+ return {
+ icon,
+ title: output.agent_name,
+ description: status ? `Status: ${status}` : output.message,
+ };
+ }
+ if (isNoResultsResponse(output)) {
+ return { icon, title: "No results" };
+ }
+ return { icon, title: "Error" };
+}
+
+export function ViewAgentOutputTool({ part }: Props) {
+ const text = getAnimationText(part);
+ const isStreaming =
+ part.state === "input-streaming" || part.state === "input-available";
+
+ const output = getViewAgentOutputToolOutput(part);
+ const isError =
+ part.state === "output-error" || (!!output && isErrorResponse(output));
+ const hasExpandableContent =
+ part.state === "output-available" &&
+ !!output &&
+ (isAgentOutputResponse(output) ||
+ isNoResultsResponse(output) ||
+ isErrorResponse(output));
+
+ return (
+
+
+
+
+
+
+ {hasExpandableContent && output && (
+
+ {isAgentOutputResponse(output) && (
+
+
+ Open
+
+ ) : null
+ }
+ >
+ {output.message}
+
+
+ {output.execution ? (
+
+
+
+ Execution
+
+
+ {output.execution.execution_id}
+
+
+ Status: {output.execution.status}
+
+
+
+ {output.execution.inputs_summary && (
+
+
+ Inputs summary
+
+
+
+
+
+ )}
+
+ {Object.entries(output.execution.outputs ?? {}).map(
+ ([key, items]) => (
+
+
+
+ {key}
+
+
+ {items.length} item
+ {items.length === 1 ? "" : "s"}
+
+
+
+ {items.slice(0, 3).map((item, i) => (
+
+ ))}
+
+
+ ),
+ )}
+
+ ) : (
+
+ No execution selected.
+
+ Try asking for a specific run or execution_id.
+
+
+ )}
+
+ )}
+
+ {isNoResultsResponse(output) && (
+
+ {output.message}
+ {output.suggestions && output.suggestions.length > 0 && (
+
+ )}
+
+ )}
+
+ {isErrorResponse(output) && (
+
+ {output.message}
+ {output.error && (
+
+ {formatMaybeJson(output.error)}
+
+ )}
+ {output.details && (
+
+ {formatMaybeJson(output.details)}
+
+ )}
+
+ )}
+
+ )}
+
+ );
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/ViewAgentOutput/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/ViewAgentOutput/helpers.tsx
new file mode 100644
index 0000000000..1ff2e64ec7
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/ViewAgentOutput/helpers.tsx
@@ -0,0 +1,158 @@
+import type { AgentOutputResponse } from "@/app/api/__generated__/models/agentOutputResponse";
+import type { ErrorResponse } from "@/app/api/__generated__/models/errorResponse";
+import type { NoResultsResponse } from "@/app/api/__generated__/models/noResultsResponse";
+import { ResponseType } from "@/app/api/__generated__/models/responseType";
+import { EyeIcon, MonitorIcon } from "@phosphor-icons/react";
+import type { ToolUIPart } from "ai";
+
+export interface ViewAgentOutputInput {
+ agent_name?: string;
+ library_agent_id?: string;
+ store_slug?: string;
+ execution_id?: string;
+ run_time?: string;
+}
+
+export type ViewAgentOutputToolOutput =
+ | AgentOutputResponse
+ | NoResultsResponse
+ | ErrorResponse;
+
+function parseOutput(output: unknown): ViewAgentOutputToolOutput | null {
+ if (!output) return null;
+ if (typeof output === "string") {
+ const trimmed = output.trim();
+ if (!trimmed) return null;
+ try {
+ return parseOutput(JSON.parse(trimmed) as unknown);
+ } catch {
+ return null;
+ }
+ }
+ if (typeof output === "object") {
+ const type = (output as { type?: unknown }).type;
+ if (
+ type === ResponseType.agent_output ||
+ type === ResponseType.no_results ||
+ type === ResponseType.error
+ ) {
+ return output as ViewAgentOutputToolOutput;
+ }
+ if ("agent_id" in output && "agent_name" in output) {
+ return output as AgentOutputResponse;
+ }
+ if ("suggestions" in output && !("error" in output)) {
+ return output as NoResultsResponse;
+ }
+ if ("error" in output || "details" in output)
+ return output as ErrorResponse;
+ }
+ return null;
+}
+
+export function isAgentOutputResponse(
+ output: ViewAgentOutputToolOutput,
+): output is AgentOutputResponse {
+ return output.type === ResponseType.agent_output || "agent_id" in output;
+}
+
+export function isNoResultsResponse(
+ output: ViewAgentOutputToolOutput,
+): output is NoResultsResponse {
+ return (
+ output.type === ResponseType.no_results ||
+ ("suggestions" in output && !("error" in output))
+ );
+}
+
+export function isErrorResponse(
+ output: ViewAgentOutputToolOutput,
+): output is ErrorResponse {
+ return output.type === ResponseType.error || "error" in output;
+}
+
+export function getViewAgentOutputToolOutput(
+ part: unknown,
+): ViewAgentOutputToolOutput | null {
+ if (!part || typeof part !== "object") return null;
+ return parseOutput((part as { output?: unknown }).output);
+}
+
+function getAgentIdentifierText(
+ input: ViewAgentOutputInput | undefined,
+): string | null {
+ if (!input) return null;
+ const libraryId = input.library_agent_id?.trim();
+ if (libraryId) return `Library agent ${libraryId}`;
+ const slug = input.store_slug?.trim();
+ if (slug) return slug;
+ const name = input.agent_name?.trim();
+ if (name) return name;
+ return null;
+}
+
+export function getAnimationText(part: {
+ state: ToolUIPart["state"];
+ input?: unknown;
+ output?: unknown;
+}): string {
+ const input = part.input as ViewAgentOutputInput | undefined;
+ const agent = getAgentIdentifierText(input);
+ const agentText = agent ? ` "${agent}"` : "";
+
+ switch (part.state) {
+ case "input-streaming":
+ case "input-available":
+ return `Retrieving agent output${agentText}`;
+ case "output-available": {
+ const output = parseOutput(part.output);
+ if (!output) return `Retrieving agent output${agentText}`;
+ if (isAgentOutputResponse(output)) {
+ if (output.execution)
+ return `Retrieved output (${output.execution.status})`;
+ return "Retrieved agent output";
+ }
+ if (isNoResultsResponse(output)) return "No outputs found";
+ return "Error loading agent output";
+ }
+ case "output-error":
+ return "Error loading agent output";
+ default:
+ return "Retrieving agent output";
+ }
+}
+
+export function ToolIcon({
+ isStreaming,
+ isError,
+}: {
+ isStreaming?: boolean;
+ isError?: boolean;
+}) {
+ return (
+
+ );
+}
+
+export function AccordionIcon() {
+ return ;
+}
+
+export function formatMaybeJson(value: unknown): string {
+ if (typeof value === "string") return value;
+ try {
+ return JSON.stringify(value, null, 2);
+ } catch {
+ return String(value);
+ }
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/useChatSession.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/useChatSession.ts
new file mode 100644
index 0000000000..ac9213e119
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/useChatSession.ts
@@ -0,0 +1,109 @@
+import {
+ getGetV2GetSessionQueryKey,
+ getGetV2ListSessionsQueryKey,
+ useGetV2GetSession,
+ usePostV2CreateSession,
+} from "@/app/api/__generated__/endpoints/chat/chat";
+import { toast } from "@/components/molecules/Toast/use-toast";
+import * as Sentry from "@sentry/nextjs";
+import { useQueryClient } from "@tanstack/react-query";
+import { parseAsString, useQueryState } from "nuqs";
+import { useEffect, useMemo, useRef } from "react";
+import { convertChatSessionMessagesToUiMessages } from "./helpers/convertChatSessionToUiMessages";
+
+export function useChatSession() {
+ const [sessionId, setSessionId] = useQueryState("sessionId", parseAsString);
+ const queryClient = useQueryClient();
+
+ const sessionQuery = useGetV2GetSession(sessionId ?? "", {
+ query: {
+ enabled: !!sessionId,
+ staleTime: Infinity,
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: false,
+ },
+ });
+
+ // When the user navigates away from a session, invalidate its query cache.
+ // useChat destroys its Chat instance on id change, so messages are lost.
+ // Invalidating ensures the next visit fetches fresh data from the API
+ // instead of hydrating from stale cache that's missing recent messages.
+ const prevSessionIdRef = useRef(sessionId);
+
+ useEffect(() => {
+ const prev = prevSessionIdRef.current;
+ prevSessionIdRef.current = sessionId;
+ if (prev && prev !== sessionId) {
+ queryClient.invalidateQueries({
+ queryKey: getGetV2GetSessionQueryKey(prev),
+ });
+ }
+ }, [sessionId, queryClient]);
+
+ // Memoize so the effect in useCopilotPage doesn't infinite-loop on a new
+ // array reference every render. Re-derives only when query data changes.
+ const hydratedMessages = useMemo(() => {
+ if (sessionQuery.data?.status !== 200 || !sessionId) return undefined;
+ return convertChatSessionMessagesToUiMessages(
+ sessionId,
+ sessionQuery.data.data.messages ?? [],
+ );
+ }, [sessionQuery.data, sessionId]);
+
+ const { mutateAsync: createSessionMutation, isPending: isCreatingSession } =
+ usePostV2CreateSession({
+ mutation: {
+ onSuccess: (response) => {
+ if (response.status === 200 && response.data?.id) {
+ setSessionId(response.data.id);
+ queryClient.invalidateQueries({
+ queryKey: getGetV2ListSessionsQueryKey(),
+ });
+ }
+ },
+ },
+ });
+
+ async function createSession() {
+ if (sessionId) return sessionId;
+ try {
+ const response = await createSessionMutation();
+ if (response.status !== 200 || !response.data?.id) {
+ const error = new Error("Failed to create session");
+ Sentry.captureException(error, {
+ extra: { status: response.status },
+ });
+ toast({
+ variant: "destructive",
+ title: "Could not start a new chat session",
+ description: "Please try again.",
+ });
+ throw error;
+ }
+ return response.data.id;
+ } catch (error) {
+ if (
+ error instanceof Error &&
+ error.message === "Failed to create session"
+ ) {
+ throw error; // already handled above
+ }
+ Sentry.captureException(error);
+ toast({
+ variant: "destructive",
+ title: "Could not start a new chat session",
+ description: "Please try again.",
+ });
+ throw error;
+ }
+ }
+
+ return {
+ sessionId,
+ setSessionId,
+ hydratedMessages,
+ isLoadingSession: sessionQuery.isLoading,
+ createSession,
+ isCreatingSession,
+ };
+}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts
index 1d9c843d7d..3dbba6e790 100644
--- a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts
@@ -1,230 +1,142 @@
-import {
- getGetV2ListSessionsQueryKey,
- postV2CreateSession,
-} from "@/app/api/__generated__/endpoints/chat/chat";
-import { useToast } from "@/components/molecules/Toast/use-toast";
-import { getHomepageRoute } from "@/lib/constants";
+import { useGetV2ListSessions } from "@/app/api/__generated__/endpoints/chat/chat";
+import { useBreakpoint } from "@/lib/hooks/useBreakpoint";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
-import {
- Flag,
- type FlagValues,
- useGetFlag,
-} from "@/services/feature-flags/use-get-flag";
-import * as Sentry from "@sentry/nextjs";
-import { useQueryClient } from "@tanstack/react-query";
-import { useFlags } from "launchdarkly-react-client-sdk";
-import { useRouter } from "next/navigation";
-import { useEffect, useReducer } from "react";
-import { useCopilotStore } from "./copilot-page-store";
-import { getGreetingName, getQuickActions, type PageState } from "./helpers";
-import { useCopilotURLState } from "./useCopilotURLState";
-
-type CopilotState = {
- pageState: PageState;
- initialPrompts: Record;
- previousSessionId: string | null;
-};
-
-type CopilotAction =
- | { type: "setPageState"; pageState: PageState }
- | { type: "setInitialPrompt"; sessionId: string; prompt: string }
- | { type: "setPreviousSessionId"; sessionId: string | null };
-
-function isSamePageState(next: PageState, current: PageState) {
- if (next.type !== current.type) return false;
- if (next.type === "creating" && current.type === "creating") {
- return next.prompt === current.prompt;
- }
- if (next.type === "chat" && current.type === "chat") {
- return (
- next.sessionId === current.sessionId &&
- next.initialPrompt === current.initialPrompt
- );
- }
- return true;
-}
-
-function copilotReducer(
- state: CopilotState,
- action: CopilotAction,
-): CopilotState {
- if (action.type === "setPageState") {
- if (isSamePageState(action.pageState, state.pageState)) return state;
- return { ...state, pageState: action.pageState };
- }
- if (action.type === "setInitialPrompt") {
- if (state.initialPrompts[action.sessionId] === action.prompt) return state;
- return {
- ...state,
- initialPrompts: {
- ...state.initialPrompts,
- [action.sessionId]: action.prompt,
- },
- };
- }
- if (action.type === "setPreviousSessionId") {
- if (state.previousSessionId === action.sessionId) return state;
- return { ...state, previousSessionId: action.sessionId };
- }
- return state;
-}
+import { useChat } from "@ai-sdk/react";
+import { DefaultChatTransport } from "ai";
+import { useEffect, useMemo, useState } from "react";
+import { useChatSession } from "./useChatSession";
export function useCopilotPage() {
- const router = useRouter();
- const queryClient = useQueryClient();
- const { user, isLoggedIn, isUserLoading } = useSupabase();
- const { toast } = useToast();
+ const { isUserLoading, isLoggedIn } = useSupabase();
+ const [isDrawerOpen, setIsDrawerOpen] = useState(false);
+ const [pendingMessage, setPendingMessage] = useState(null);
- const isNewChatModalOpen = useCopilotStore((s) => s.isNewChatModalOpen);
- const setIsStreaming = useCopilotStore((s) => s.setIsStreaming);
- const cancelNewChat = useCopilotStore((s) => s.cancelNewChat);
+ const {
+ sessionId,
+ setSessionId,
+ hydratedMessages,
+ isLoadingSession,
+ createSession,
+ isCreatingSession,
+ } = useChatSession();
- const isChatEnabled = useGetFlag(Flag.CHAT);
- const flags = useFlags();
- const homepageRoute = getHomepageRoute(isChatEnabled);
- const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true";
- const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID;
- const isLaunchDarklyConfigured = envEnabled && Boolean(clientId);
- const isFlagReady =
- !isLaunchDarklyConfigured || flags[Flag.CHAT] !== undefined;
+ const breakpoint = useBreakpoint();
+ const isMobile =
+ breakpoint === "base" || breakpoint === "sm" || breakpoint === "md";
- const [state, dispatch] = useReducer(copilotReducer, {
- pageState: { type: "welcome" },
- initialPrompts: {},
- previousSessionId: null,
- });
-
- const greetingName = getGreetingName(user);
- const quickActions = getQuickActions();
-
- function setPageState(pageState: PageState) {
- dispatch({ type: "setPageState", pageState });
- }
-
- function setInitialPrompt(sessionId: string, prompt: string) {
- dispatch({ type: "setInitialPrompt", sessionId, prompt });
- }
-
- function setPreviousSessionId(sessionId: string | null) {
- dispatch({ type: "setPreviousSessionId", sessionId });
- }
-
- const { setUrlSessionId } = useCopilotURLState({
- pageState: state.pageState,
- initialPrompts: state.initialPrompts,
- previousSessionId: state.previousSessionId,
- setPageState,
- setInitialPrompt,
- setPreviousSessionId,
- });
-
- useEffect(
- function transitionNewChatToWelcome() {
- if (state.pageState.type === "newChat") {
- function setWelcomeState() {
- dispatch({ type: "setPageState", pageState: { type: "welcome" } });
- }
-
- const timer = setTimeout(setWelcomeState, 300);
-
- return function cleanup() {
- clearTimeout(timer);
- };
- }
- },
- [state.pageState.type],
+ const transport = useMemo(
+ () =>
+ sessionId
+ ? new DefaultChatTransport({
+ api: `/api/chat/sessions/${sessionId}/stream`,
+ prepareSendMessagesRequest: ({ messages }) => {
+ const last = messages[messages.length - 1];
+ return {
+ body: {
+ message: (
+ last.parts?.map((p) => (p.type === "text" ? p.text : "")) ??
+ []
+ ).join(""),
+ is_user_message: last.role === "user",
+ context: null,
+ },
+ };
+ },
+ })
+ : null,
+ [sessionId],
);
- useEffect(
- function ensureAccess() {
- if (!isFlagReady) return;
- if (isChatEnabled === false) {
- router.replace(homepageRoute);
- }
- },
- [homepageRoute, isChatEnabled, isFlagReady, router],
- );
+ const { messages, sendMessage, stop, status, error, setMessages } = useChat({
+ id: sessionId ?? undefined,
+ transport: transport ?? undefined,
+ });
- async function startChatWithPrompt(prompt: string) {
- if (!prompt?.trim()) return;
- if (state.pageState.type === "creating") return;
-
- const trimmedPrompt = prompt.trim();
- dispatch({
- type: "setPageState",
- pageState: { type: "creating", prompt: trimmedPrompt },
+ useEffect(() => {
+ if (!hydratedMessages || hydratedMessages.length === 0) return;
+ setMessages((prev) => {
+ if (prev.length >= hydratedMessages.length) return prev;
+ return hydratedMessages;
});
+ }, [hydratedMessages, setMessages]);
- try {
- const sessionResponse = await postV2CreateSession({
- body: JSON.stringify({}),
- });
+ // Clear messages when session is null
+ useEffect(() => {
+ if (!sessionId) setMessages([]);
+ }, [sessionId, setMessages]);
- if (sessionResponse.status !== 200 || !sessionResponse.data?.id) {
- throw new Error("Failed to create session");
- }
+ useEffect(() => {
+ if (!sessionId || !pendingMessage) return;
+ const msg = pendingMessage;
+ setPendingMessage(null);
+ sendMessage({ text: msg });
+ }, [sessionId, pendingMessage, sendMessage]);
- const sessionId = sessionResponse.data.id;
+ async function onSend(message: string) {
+ const trimmed = message.trim();
+ if (!trimmed) return;
- dispatch({
- type: "setInitialPrompt",
- sessionId,
- prompt: trimmedPrompt,
- });
-
- await queryClient.invalidateQueries({
- queryKey: getGetV2ListSessionsQueryKey(),
- });
-
- await setUrlSessionId(sessionId, { shallow: false });
- dispatch({
- type: "setPageState",
- pageState: { type: "chat", sessionId, initialPrompt: trimmedPrompt },
- });
- } catch (error) {
- console.error("[CopilotPage] Failed to start chat:", error);
- toast({ title: "Failed to start chat", variant: "destructive" });
- Sentry.captureException(error);
- dispatch({ type: "setPageState", pageState: { type: "welcome" } });
+ if (sessionId) {
+ sendMessage({ text: trimmed });
+ return;
}
+
+ setPendingMessage(trimmed);
+ await createSession();
}
- function handleQuickAction(action: string) {
- startChatWithPrompt(action);
+ const { data: sessionsResponse, isLoading: isLoadingSessions } =
+ useGetV2ListSessions(
+ { limit: 50 },
+ { query: { enabled: !isUserLoading && isLoggedIn } },
+ );
+
+ const sessions =
+ sessionsResponse?.status === 200 ? sessionsResponse.data.sessions : [];
+
+ function handleOpenDrawer() {
+ setIsDrawerOpen(true);
}
- function handleSessionNotFound() {
- router.replace("/copilot");
+ function handleCloseDrawer() {
+ setIsDrawerOpen(false);
}
- function handleStreamingChange(isStreamingValue: boolean) {
- setIsStreaming(isStreamingValue);
+ function handleDrawerOpenChange(open: boolean) {
+ setIsDrawerOpen(open);
}
- function handleCancelNewChat() {
- cancelNewChat();
+ function handleSelectSession(id: string) {
+ setSessionId(id);
+ if (isMobile) setIsDrawerOpen(false);
}
- function handleNewChatModalOpen(isOpen: boolean) {
- if (!isOpen) cancelNewChat();
+ function handleNewChat() {
+ setSessionId(null);
+ if (isMobile) setIsDrawerOpen(false);
}
return {
- state: {
- greetingName,
- quickActions,
- isLoading: isUserLoading,
- pageState: state.pageState,
- isNewChatModalOpen,
- isReady: isFlagReady && isChatEnabled !== false && isLoggedIn,
- },
- handlers: {
- handleQuickAction,
- startChatWithPrompt,
- handleSessionNotFound,
- handleStreamingChange,
- handleCancelNewChat,
- handleNewChatModalOpen,
- },
+ sessionId,
+ messages,
+ status,
+ error,
+ stop,
+ isLoadingSession,
+ isCreatingSession,
+ isUserLoading,
+ isLoggedIn,
+ createSession,
+ onSend,
+ // Mobile drawer
+ isMobile,
+ isDrawerOpen,
+ sessions,
+ isLoadingSessions,
+ handleOpenDrawer,
+ handleCloseDrawer,
+ handleDrawerOpenChange,
+ handleSelectSession,
+ handleNewChat,
};
}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotURLState.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotURLState.ts
deleted file mode 100644
index 5e37e29a15..0000000000
--- a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotURLState.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import { parseAsString, useQueryState } from "nuqs";
-import { useLayoutEffect } from "react";
-import {
- getInitialPromptFromState,
- type PageState,
- shouldResetToWelcome,
-} from "./helpers";
-
-interface UseCopilotUrlStateArgs {
- pageState: PageState;
- initialPrompts: Record;
- previousSessionId: string | null;
- setPageState: (pageState: PageState) => void;
- setInitialPrompt: (sessionId: string, prompt: string) => void;
- setPreviousSessionId: (sessionId: string | null) => void;
-}
-
-export function useCopilotURLState({
- pageState,
- initialPrompts,
- previousSessionId,
- setPageState,
- setInitialPrompt,
- setPreviousSessionId,
-}: UseCopilotUrlStateArgs) {
- const [urlSessionId, setUrlSessionId] = useQueryState(
- "sessionId",
- parseAsString,
- );
-
- function syncSessionFromUrl() {
- if (urlSessionId) {
- if (pageState.type === "chat" && pageState.sessionId === urlSessionId) {
- setPreviousSessionId(urlSessionId);
- return;
- }
-
- const storedInitialPrompt = initialPrompts[urlSessionId];
- const currentInitialPrompt = getInitialPromptFromState(
- pageState,
- storedInitialPrompt,
- );
-
- if (currentInitialPrompt) {
- setInitialPrompt(urlSessionId, currentInitialPrompt);
- }
-
- setPageState({
- type: "chat",
- sessionId: urlSessionId,
- initialPrompt: currentInitialPrompt,
- });
- setPreviousSessionId(urlSessionId);
- return;
- }
-
- const wasInChat = previousSessionId !== null && pageState.type === "chat";
- setPreviousSessionId(null);
- if (wasInChat) {
- setPageState({ type: "newChat" });
- return;
- }
-
- if (shouldResetToWelcome(pageState)) {
- setPageState({ type: "welcome" });
- }
- }
-
- useLayoutEffect(syncSessionFromUrl, [
- urlSessionId,
- pageState.type,
- previousSessionId,
- initialPrompts,
- ]);
-
- return {
- urlSessionId,
- setUrlSessionId,
- };
-}
diff --git a/autogpt_platform/frontend/src/app/(platform)/error/page.tsx b/autogpt_platform/frontend/src/app/(platform)/error/page.tsx
index b26ca4559b..3cf68178ad 100644
--- a/autogpt_platform/frontend/src/app/(platform)/error/page.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/error/page.tsx
@@ -1,8 +1,6 @@
"use client";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
-import { getHomepageRoute } from "@/lib/constants";
-import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import { useSearchParams } from "next/navigation";
import { Suspense } from "react";
import { getErrorDetails } from "./helpers";
@@ -11,8 +9,6 @@ function ErrorPageContent() {
const searchParams = useSearchParams();
const errorMessage = searchParams.get("message");
const errorDetails = getErrorDetails(errorMessage);
- const isChatEnabled = useGetFlag(Flag.CHAT);
- const homepageRoute = getHomepageRoute(isChatEnabled);
function handleRetry() {
// Auth-related errors should redirect to login
@@ -30,7 +26,7 @@ function ErrorPageContent() {
}, 2000);
} else {
// For server/network errors, go to home
- window.location.href = homepageRoute;
+ window.location.href = "/";
}
}
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/RunDetailHeader/RunDetailHeader.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/RunDetailHeader/RunDetailHeader.tsx
index 3d04234bb3..4e98c70de5 100644
--- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/RunDetailHeader/RunDetailHeader.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/RunDetailHeader/RunDetailHeader.tsx
@@ -2,7 +2,7 @@ import { GraphExecution } from "@/app/api/__generated__/models/graphExecution";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { Text } from "@/components/atoms/Text/Text";
import { ClockClockwiseIcon } from "@phosphor-icons/react";
-import moment from "moment";
+import { formatDistanceToNow, formatDistanceStrict } from "date-fns";
import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers";
import { RunStatusBadge } from "../SelectedRunView/components/RunStatusBadge";
@@ -43,7 +43,10 @@ export function RunDetailHeader({ agent, run, scheduleRecurrence }: Props) {
{run ? (
- Started {moment(run.started_at).fromNow()}
+ Started{" "}
+ {run.started_at
+ ? formatDistanceToNow(run.started_at, { addSuffix: true })
+ : "—"}
|
@@ -62,7 +65,7 @@ export function RunDetailHeader({ agent, run, scheduleRecurrence }: Props) {
|
Duration:{" "}
- {moment.duration(run.stats.duration, "seconds").humanize()}
+ {formatDistanceStrict(0, run.stats.duration * 1000)}
>
)}
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/components/ScheduleListItem.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/components/ScheduleListItem.tsx
index a389fb4fc8..1ad40fcef4 100644
--- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/components/ScheduleListItem.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/components/ScheduleListItem.tsx
@@ -3,7 +3,7 @@
import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { ClockClockwiseIcon } from "@phosphor-icons/react";
-import moment from "moment";
+import { formatDistanceToNow } from "date-fns";
import { IconWrapper } from "./IconWrapper";
import { ScheduleActionsDropdown } from "./ScheduleActionsDropdown";
import { SidebarItemCard } from "./SidebarItemCard";
@@ -26,7 +26,9 @@ export function ScheduleListItem({
return (
}
title={template.name}
- description={moment(template.updated_at).fromNow()}
+ description={formatDistanceToNow(template.updated_at, {
+ addSuffix: true,
+ })}
onClick={onClick}
selected={selected}
actions={
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/components/TriggerListItem.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/components/TriggerListItem.tsx
index 4c399e640a..c7b0473a9b 100644
--- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/components/TriggerListItem.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/components/TriggerListItem.tsx
@@ -3,7 +3,7 @@
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
import { LightningIcon } from "@phosphor-icons/react";
-import moment from "moment";
+import { formatDistanceToNow } from "date-fns";
import { IconWrapper } from "./IconWrapper";
import { SidebarItemCard } from "./SidebarItemCard";
import { TriggerActionsDropdown } from "./TriggerActionsDropdown";
@@ -31,7 +31,7 @@ export function TriggerListItem({
}
title={trigger.name}
- description={moment(trigger.updated_at).fromNow()}
+ description={formatDistanceToNow(trigger.updated_at, { addSuffix: true })}
onClick={onClick}
selected={selected}
actions={
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-details-view.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-details-view.tsx
index f51c44c243..eb5224c958 100644
--- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-details-view.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-details-view.tsx
@@ -1,5 +1,5 @@
"use client";
-import moment from "moment";
+import { format, formatDistanceToNow, formatDistanceStrict } from "date-fns";
import React, { useCallback, useMemo, useEffect } from "react";
import {
@@ -90,13 +90,15 @@ export function AgentRunDetailsView({
},
{
label: "Started",
- value: `${moment(run.started_at).fromNow()}, ${moment(run.started_at).format("HH:mm")}`,
+ value: run.started_at
+ ? `${formatDistanceToNow(run.started_at, { addSuffix: true })}, ${format(run.started_at, "HH:mm")}`
+ : "—",
},
...(run.stats
? [
{
label: "Duration",
- value: moment.duration(run.stats.duration, "seconds").humanize(),
+ value: formatDistanceStrict(0, run.stats.duration * 1000),
},
{ label: "Steps", value: run.stats.node_exec_count },
{ label: "Cost", value: formatCredits(run.stats.cost) },
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-draft-view.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-draft-view.tsx
index 0147c19a5c..b0c3a6ff7b 100644
--- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-draft-view.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-draft-view.tsx
@@ -10,8 +10,8 @@ import React, {
import {
CredentialsMetaInput,
CredentialsType,
+ Graph,
GraphExecutionID,
- GraphMeta,
LibraryAgentPreset,
LibraryAgentPresetID,
LibraryAgentPresetUpdatable,
@@ -69,7 +69,7 @@ export function AgentRunDraftView({
className,
recommendedScheduleCron,
}: {
- graph: GraphMeta;
+ graph: Graph;
agentActions?: ButtonAction[];
recommendedScheduleCron?: string | null;
doRun?: (
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-summary-card.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-summary-card.tsx
index 423495878b..6f7d7865bc 100644
--- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-summary-card.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-run-summary-card.tsx
@@ -1,5 +1,5 @@
import React from "react";
-import moment from "moment";
+import { formatDistanceToNow, isPast } from "date-fns";
import { cn } from "@/lib/utils";
@@ -118,10 +118,10 @@ export function AgentRunSummaryCard({
{timestamp && (
- {moment(timestamp).isBefore() ? "Ran" : "Runs in"}{" "}
- {moment(timestamp).fromNow()}
+ {isPast(timestamp) ? "Ran" : "Runs in"}{" "}
+ {formatDistanceToNow(timestamp, { addSuffix: true })}
)}
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-schedule-details-view.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-schedule-details-view.tsx
index 61161088fc..30b0a82e65 100644
--- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-schedule-details-view.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-schedule-details-view.tsx
@@ -2,8 +2,8 @@
import React, { useCallback, useMemo } from "react";
import {
+ Graph,
GraphExecutionID,
- GraphMeta,
Schedule,
ScheduleID,
} from "@/lib/autogpt-server-api";
@@ -35,7 +35,7 @@ export function AgentScheduleDetailsView({
onForcedRun,
doDeleteSchedule,
}: {
- graph: GraphMeta;
+ graph: Graph;
schedule: Schedule;
agentActions: ButtonAction[];
onForcedRun: (runID: GraphExecutionID) => void;
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/useLibrarySearchbar.tsx b/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/useLibrarySearchbar.tsx
index 74b8e9874c..efeb654ed5 100644
--- a/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/useLibrarySearchbar.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/library/components/LibrarySearchBar/useLibrarySearchbar.tsx
@@ -1,4 +1,4 @@
-import { debounce } from "lodash";
+import debounce from "lodash/debounce";
import { useCallback, useEffect } from "react";
interface Props {
diff --git a/autogpt_platform/frontend/src/app/(platform)/login/actions.ts b/autogpt_platform/frontend/src/app/(platform)/login/actions.ts
index 936c879d69..c4867dd123 100644
--- a/autogpt_platform/frontend/src/app/(platform)/login/actions.ts
+++ b/autogpt_platform/frontend/src/app/(platform)/login/actions.ts
@@ -4,7 +4,7 @@ import BackendAPI from "@/lib/autogpt-server-api";
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
import { loginFormSchema } from "@/types/auth";
import * as Sentry from "@sentry/nextjs";
-import { shouldShowOnboarding } from "../../api/helpers";
+import { getOnboardingStatus } from "../../api/helpers";
export async function login(email: string, password: string) {
try {
@@ -36,11 +36,13 @@ export async function login(email: string, password: string) {
const api = new BackendAPI();
await api.createUser();
- const onboarding = await shouldShowOnboarding();
+ // Get onboarding status from backend (includes chat flag evaluated for this user)
+ const { shouldShowOnboarding } = await getOnboardingStatus();
+ const next = shouldShowOnboarding ? "/onboarding" : "/";
return {
success: true,
- onboarding,
+ next,
};
} catch (err) {
Sentry.captureException(err);
diff --git a/autogpt_platform/frontend/src/app/(platform)/login/useLoginPage.ts b/autogpt_platform/frontend/src/app/(platform)/login/useLoginPage.ts
index 9bde570548..9b81965c31 100644
--- a/autogpt_platform/frontend/src/app/(platform)/login/useLoginPage.ts
+++ b/autogpt_platform/frontend/src/app/(platform)/login/useLoginPage.ts
@@ -1,8 +1,6 @@
import { useToast } from "@/components/molecules/Toast/use-toast";
-import { getHomepageRoute } from "@/lib/constants";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { environment } from "@/services/environment";
-import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import { loginFormSchema, LoginProvider } from "@/types/auth";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter, useSearchParams } from "next/navigation";
@@ -22,17 +20,15 @@ export function useLoginPage() {
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
const [showNotAllowedModal, setShowNotAllowedModal] = useState(false);
const isCloudEnv = environment.isCloud();
- const isChatEnabled = useGetFlag(Flag.CHAT);
- const homepageRoute = getHomepageRoute(isChatEnabled);
// Get redirect destination from 'next' query parameter
const nextUrl = searchParams.get("next");
useEffect(() => {
if (isLoggedIn && !isLoggingIn) {
- router.push(nextUrl || homepageRoute);
+ router.push(nextUrl || "/");
}
- }, [homepageRoute, isLoggedIn, isLoggingIn, nextUrl, router]);
+ }, [isLoggedIn, isLoggingIn, nextUrl, router]);
const form = useForm>({
resolver: zodResolver(loginFormSchema),
@@ -97,13 +93,8 @@ export function useLoginPage() {
throw new Error(result.error || "Login failed");
}
- if (nextUrl) {
- router.replace(nextUrl);
- } else if (result.onboarding) {
- router.replace("/onboarding");
- } else {
- router.replace(homepageRoute);
- }
+ // Prefer URL's next parameter, then use backend-determined route
+ router.replace(nextUrl || result.next || "/");
} catch (error) {
toast({
title:
diff --git a/autogpt_platform/frontend/src/app/(platform)/monitoring/components/AgentFlowList.tsx b/autogpt_platform/frontend/src/app/(platform)/monitoring/components/AgentFlowList.tsx
index badba61bf1..a306275f10 100644
--- a/autogpt_platform/frontend/src/app/(platform)/monitoring/components/AgentFlowList.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/monitoring/components/AgentFlowList.tsx
@@ -30,7 +30,7 @@ import {
TableHeader,
TableRow,
} from "@/components/__legacy__/ui/table";
-import moment from "moment/moment";
+import { formatDistanceToNow } from "date-fns";
import { DialogTitle } from "@/components/__legacy__/ui/dialog";
import { AgentImportForm } from "./AgentImportForm";
@@ -161,8 +161,12 @@ export const AgentFlowList = ({
(!lastRun ? (
) : (
-
- {moment(lastRun.started_at).fromNow()}
+
+ {lastRun.started_at
+ ? formatDistanceToNow(lastRun.started_at, {
+ addSuffix: true,
+ })
+ : "—"}
))}
diff --git a/autogpt_platform/frontend/src/app/(platform)/monitoring/components/FlowRunInfo.tsx b/autogpt_platform/frontend/src/app/(platform)/monitoring/components/FlowRunInfo.tsx
index e619c2fd3d..0fdf39e8d9 100644
--- a/autogpt_platform/frontend/src/app/(platform)/monitoring/components/FlowRunInfo.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/monitoring/components/FlowRunInfo.tsx
@@ -10,7 +10,7 @@ import Link from "next/link";
import { Button, buttonVariants } from "@/components/__legacy__/ui/button";
import { IconSquare } from "@/components/__legacy__/ui/icons";
import { ExitIcon, Pencil2Icon } from "@radix-ui/react-icons";
-import moment from "moment/moment";
+import { format } from "date-fns";
import { FlowRunStatusBadge } from "@/app/(platform)/monitoring/components/FlowRunStatusBadge";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import RunnerOutputUI, {
@@ -111,11 +111,15 @@ export const FlowRunInfo: React.FC<
Started:{" "}
- {moment(execution.started_at).format("YYYY-MM-DD HH:mm:ss")}
+ {execution.started_at
+ ? format(execution.started_at, "yyyy-MM-dd HH:mm:ss")
+ : "—"}
Finished:{" "}
- {moment(execution.ended_at).format("YYYY-MM-DD HH:mm:ss")}
+ {execution.ended_at
+ ? format(execution.ended_at, "yyyy-MM-dd HH:mm:ss")
+ : "—"}
{execution.stats && (
diff --git a/autogpt_platform/frontend/src/app/(platform)/monitoring/components/FlowRunsList.tsx b/autogpt_platform/frontend/src/app/(platform)/monitoring/components/FlowRunsList.tsx
index a99d9309b5..c4a2a40447 100644
--- a/autogpt_platform/frontend/src/app/(platform)/monitoring/components/FlowRunsList.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/monitoring/components/FlowRunsList.tsx
@@ -14,7 +14,7 @@ import {
TableHeader,
TableRow,
} from "@/components/__legacy__/ui/table";
-import moment from "moment/moment";
+import { format } from "date-fns";
import { FlowRunStatusBadge } from "@/app/(platform)/monitoring/components/FlowRunStatusBadge";
import { TextRenderer } from "../../../../components/__legacy__/ui/render";
@@ -59,7 +59,9 @@ export const FlowRunsList: React.FC<{
/>
- {moment(execution.started_at).format("HH:mm")}
+ {execution.started_at
+ ? format(execution.started_at, "HH:mm")
+ : "—"}
{
- const now = moment();
- const time = moment(unixTime);
- return now.diff(time, "hours") < 24
- ? time.format("HH:mm")
- : time.format("YYYY-MM-DD HH:mm");
+ const now = new Date();
+ const time = new Date(unixTime);
+ return differenceInHours(now, time) < 24
+ ? format(time, "HH:mm")
+ : format(time, "yyyy-MM-dd HH:mm");
}}
name="Time"
scale="time"
@@ -79,7 +79,9 @@ export const FlowRunsTimeline = ({
Started:{" "}
- {moment(data.started_at).format("YYYY-MM-DD HH:mm:ss")}
+ {data.started_at
+ ? format(data.started_at, "yyyy-MM-dd HH:mm:ss")
+ : "—"}
{data.stats && (
diff --git a/autogpt_platform/frontend/src/app/(platform)/signup/actions.ts b/autogpt_platform/frontend/src/app/(platform)/signup/actions.ts
index 6d68782e7a..204482dbe9 100644
--- a/autogpt_platform/frontend/src/app/(platform)/signup/actions.ts
+++ b/autogpt_platform/frontend/src/app/(platform)/signup/actions.ts
@@ -1,18 +1,16 @@
"use server";
-import { getHomepageRoute } from "@/lib/constants";
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
import { signupFormSchema } from "@/types/auth";
import * as Sentry from "@sentry/nextjs";
import { isWaitlistError, logWaitlistError } from "../../api/auth/utils";
-import { shouldShowOnboarding } from "../../api/helpers";
+import { getOnboardingStatus } from "../../api/helpers";
export async function signup(
email: string,
password: string,
confirmPassword: string,
agreeToTerms: boolean,
- isChatEnabled: boolean,
) {
try {
const parsed = signupFormSchema.safeParse({
@@ -59,10 +57,9 @@ export async function signup(
await supabase.auth.setSession(data.session);
}
- const isOnboardingEnabled = await shouldShowOnboarding();
- const next = isOnboardingEnabled
- ? "/onboarding"
- : getHomepageRoute(isChatEnabled);
+ // Get onboarding status from backend (includes chat flag evaluated for this user)
+ const { shouldShowOnboarding } = await getOnboardingStatus();
+ const next = shouldShowOnboarding ? "/onboarding" : "/";
return { success: true, next };
} catch (err) {
diff --git a/autogpt_platform/frontend/src/app/(platform)/signup/useSignupPage.ts b/autogpt_platform/frontend/src/app/(platform)/signup/useSignupPage.ts
index 5bd53ca846..fd78b48735 100644
--- a/autogpt_platform/frontend/src/app/(platform)/signup/useSignupPage.ts
+++ b/autogpt_platform/frontend/src/app/(platform)/signup/useSignupPage.ts
@@ -1,8 +1,6 @@
import { useToast } from "@/components/molecules/Toast/use-toast";
-import { getHomepageRoute } from "@/lib/constants";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { environment } from "@/services/environment";
-import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import { LoginProvider, signupFormSchema } from "@/types/auth";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter, useSearchParams } from "next/navigation";
@@ -22,17 +20,15 @@ export function useSignupPage() {
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
const [showNotAllowedModal, setShowNotAllowedModal] = useState(false);
const isCloudEnv = environment.isCloud();
- const isChatEnabled = useGetFlag(Flag.CHAT);
- const homepageRoute = getHomepageRoute(isChatEnabled);
// Get redirect destination from 'next' query parameter
const nextUrl = searchParams.get("next");
useEffect(() => {
if (isLoggedIn && !isSigningUp) {
- router.push(nextUrl || homepageRoute);
+ router.push(nextUrl || "/");
}
- }, [homepageRoute, isLoggedIn, isSigningUp, nextUrl, router]);
+ }, [isLoggedIn, isSigningUp, nextUrl, router]);
const form = useForm>({
resolver: zodResolver(signupFormSchema),
@@ -108,7 +104,6 @@ export function useSignupPage() {
data.password,
data.confirmPassword,
data.agreeToTerms,
- isChatEnabled === true,
);
setIsLoading(false);
@@ -134,7 +129,7 @@ export function useSignupPage() {
}
// Prefer the URL's next parameter, then result.next (for onboarding), then default
- const redirectTo = nextUrl || result.next || homepageRoute;
+ const redirectTo = nextUrl || result.next || "/";
router.replace(redirectTo);
} catch (error) {
setIsLoading(false);
diff --git a/autogpt_platform/frontend/src/app/api/chat/sessions/[sessionId]/stream/route.ts b/autogpt_platform/frontend/src/app/api/chat/sessions/[sessionId]/stream/route.ts
index d63eed0ca2..6facf80c58 100644
--- a/autogpt_platform/frontend/src/app/api/chat/sessions/[sessionId]/stream/route.ts
+++ b/autogpt_platform/frontend/src/app/api/chat/sessions/[sessionId]/stream/route.ts
@@ -88,39 +88,27 @@ export async function POST(
}
/**
- * Legacy GET endpoint for backward compatibility
+ * Resume an active stream for a session.
+ *
+ * Called by the AI SDK's `useChat(resume: true)` on page load.
+ * Proxies to the backend which checks for an active stream and either
+ * replays it (200 + SSE) or returns 204 No Content.
*/
export async function GET(
- request: NextRequest,
+ _request: NextRequest,
{ params }: { params: Promise<{ sessionId: string }> },
) {
const { sessionId } = await params;
- const searchParams = request.nextUrl.searchParams;
- const message = searchParams.get("message");
- const isUserMessage = searchParams.get("is_user_message");
-
- if (!message) {
- return new Response("Missing message parameter", { status: 400 });
- }
try {
- // Get auth token from server-side session
const token = await getServerAuthToken();
- // Build backend URL
const backendUrl = environment.getAGPTServerBaseUrl();
const streamUrl = new URL(
`/api/chat/sessions/${sessionId}/stream`,
backendUrl,
);
- streamUrl.searchParams.set("message", message);
- // Pass is_user_message parameter if provided
- if (isUserMessage !== null) {
- streamUrl.searchParams.set("is_user_message", isUserMessage);
- }
-
- // Forward request to backend with auth header
const headers: Record = {
Accept: "text/event-stream",
"Cache-Control": "no-cache",
@@ -136,6 +124,11 @@ export async function GET(
headers,
});
+ // 204 = no active stream to resume
+ if (response.status === 204) {
+ return new Response(null, { status: 204 });
+ }
+
if (!response.ok) {
const error = await response.text();
return new Response(error, {
@@ -144,17 +137,17 @@ export async function GET(
});
}
- // Return the SSE stream directly
return new Response(response.body, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
"X-Accel-Buffering": "no",
+ "x-vercel-ai-ui-message-stream": "v1",
},
});
} catch (error) {
- console.error("SSE proxy error:", error);
+ console.error("Resume stream proxy error:", error);
return new Response(
JSON.stringify({
error: "Failed to connect to chat service",
diff --git a/autogpt_platform/frontend/src/app/api/chat/tasks/[taskId]/stream/route.ts b/autogpt_platform/frontend/src/app/api/chat/tasks/[taskId]/stream/route.ts
new file mode 100644
index 0000000000..336786bfdb
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/api/chat/tasks/[taskId]/stream/route.ts
@@ -0,0 +1,81 @@
+import { environment } from "@/services/environment";
+import { getServerAuthToken } from "@/lib/autogpt-server-api/helpers";
+import { NextRequest } from "next/server";
+
+/**
+ * SSE Proxy for task stream reconnection.
+ *
+ * This endpoint allows clients to reconnect to an ongoing or recently completed
+ * background task's stream. It replays missed messages from Redis Streams and
+ * subscribes to live updates if the task is still running.
+ *
+ * Client contract:
+ * 1. When receiving an operation_started event, store the task_id
+ * 2. To reconnect: GET /api/chat/tasks/{taskId}/stream?last_message_id={idx}
+ * 3. Messages are replayed from the last_message_id position
+ * 4. Stream ends when "finish" event is received
+ */
+export async function GET(
+ request: NextRequest,
+ { params }: { params: Promise<{ taskId: string }> },
+) {
+ const { taskId } = await params;
+ const searchParams = request.nextUrl.searchParams;
+ const lastMessageId = searchParams.get("last_message_id") || "0-0";
+
+ try {
+ // Get auth token from server-side session
+ const token = await getServerAuthToken();
+
+ // Build backend URL
+ const backendUrl = environment.getAGPTServerBaseUrl();
+ const streamUrl = new URL(`/api/chat/tasks/${taskId}/stream`, backendUrl);
+ streamUrl.searchParams.set("last_message_id", lastMessageId);
+
+ // Forward request to backend with auth header
+ const headers: Record = {
+ Accept: "text/event-stream",
+ "Cache-Control": "no-cache",
+ Connection: "keep-alive",
+ };
+
+ if (token) {
+ headers["Authorization"] = `Bearer ${token}`;
+ }
+
+ const response = await fetch(streamUrl.toString(), {
+ method: "GET",
+ headers,
+ });
+
+ if (!response.ok) {
+ const error = await response.text();
+ return new Response(error, {
+ status: response.status,
+ headers: { "Content-Type": "application/json" },
+ });
+ }
+
+ // Return the SSE stream directly
+ return new Response(response.body, {
+ headers: {
+ "Content-Type": "text/event-stream",
+ "Cache-Control": "no-cache, no-transform",
+ Connection: "keep-alive",
+ "X-Accel-Buffering": "no",
+ },
+ });
+ } catch (error) {
+ console.error("Task stream proxy error:", error);
+ return new Response(
+ JSON.stringify({
+ error: "Failed to connect to task stream",
+ detail: error instanceof Error ? error.message : String(error),
+ }),
+ {
+ status: 500,
+ headers: { "Content-Type": "application/json" },
+ },
+ );
+ }
+}
diff --git a/autogpt_platform/frontend/src/app/api/helpers.ts b/autogpt_platform/frontend/src/app/api/helpers.ts
index e9a708ba4c..226f5fa786 100644
--- a/autogpt_platform/frontend/src/app/api/helpers.ts
+++ b/autogpt_platform/frontend/src/app/api/helpers.ts
@@ -175,9 +175,11 @@ export async function resolveResponse<
return res.data;
}
-export async function shouldShowOnboarding() {
- const isEnabled = await resolveResponse(getV1IsOnboardingEnabled());
+export async function getOnboardingStatus() {
+ const status = await resolveResponse(getV1IsOnboardingEnabled());
const onboarding = await resolveResponse(getV1OnboardingState());
const isCompleted = onboarding.completedSteps.includes("CONGRATS");
- return isEnabled && !isCompleted;
+ return {
+ shouldShowOnboarding: status.is_onboarding_enabled && !isCompleted,
+ };
}
diff --git a/autogpt_platform/frontend/src/app/api/proxy/[...path]/route.ts b/autogpt_platform/frontend/src/app/api/proxy/[...path]/route.ts
index 293c406373..442bd77e0f 100644
--- a/autogpt_platform/frontend/src/app/api/proxy/[...path]/route.ts
+++ b/autogpt_platform/frontend/src/app/api/proxy/[...path]/route.ts
@@ -1,5 +1,6 @@
import {
ApiError,
+ getServerAuthToken,
makeAuthenticatedFileUpload,
makeAuthenticatedRequest,
} from "@/lib/autogpt-server-api/helpers";
@@ -15,6 +16,69 @@ function buildBackendUrl(path: string[], queryString: string): string {
return `${environment.getAGPTServerBaseUrl()}/${backendPath}${queryString}`;
}
+/**
+ * Check if this is a workspace file download request that needs binary response handling.
+ */
+function isWorkspaceDownloadRequest(path: string[]): boolean {
+ // Match pattern: api/workspace/files/{id}/download (5 segments)
+ return (
+ path.length == 5 &&
+ path[0] === "api" &&
+ path[1] === "workspace" &&
+ path[2] === "files" &&
+ path[path.length - 1] === "download"
+ );
+}
+
+/**
+ * Handle workspace file download requests with proper binary response streaming.
+ */
+async function handleWorkspaceDownload(
+ req: NextRequest,
+ backendUrl: string,
+): Promise {
+ const token = await getServerAuthToken();
+
+ const headers: Record = {};
+ if (token && token !== "no-token-found") {
+ headers["Authorization"] = `Bearer ${token}`;
+ }
+
+ const response = await fetch(backendUrl, {
+ method: "GET",
+ headers,
+ redirect: "follow", // Follow redirects to signed URLs
+ });
+
+ if (!response.ok) {
+ return NextResponse.json(
+ { error: `Failed to download file: ${response.statusText}` },
+ { status: response.status },
+ );
+ }
+
+ // Get the content type from the backend response
+ const contentType =
+ response.headers.get("Content-Type") || "application/octet-stream";
+ const contentDisposition = response.headers.get("Content-Disposition");
+
+ // Stream the response body
+ const responseHeaders: Record = {
+ "Content-Type": contentType,
+ };
+
+ if (contentDisposition) {
+ responseHeaders["Content-Disposition"] = contentDisposition;
+ }
+
+ // Return the binary content
+ const arrayBuffer = await response.arrayBuffer();
+ return new NextResponse(arrayBuffer, {
+ status: 200,
+ headers: responseHeaders,
+ });
+}
+
async function handleJsonRequest(
req: NextRequest,
method: string,
@@ -180,6 +244,11 @@ async function handler(
};
try {
+ // Handle workspace file downloads separately (binary response)
+ if (method === "GET" && isWorkspaceDownloadRequest(path)) {
+ return await handleWorkspaceDownload(req, backendUrl);
+ }
+
if (method === "GET" || method === "DELETE") {
responseBody = await handleGetDeleteRequest(method, backendUrl, req);
} else if (contentType?.includes("application/json")) {
diff --git a/autogpt_platform/frontend/src/app/api/transcribe/route.ts b/autogpt_platform/frontend/src/app/api/transcribe/route.ts
new file mode 100644
index 0000000000..10c182cdfa
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/api/transcribe/route.ts
@@ -0,0 +1,77 @@
+import { getServerAuthToken } from "@/lib/autogpt-server-api/helpers";
+import { NextRequest, NextResponse } from "next/server";
+
+const WHISPER_API_URL = "https://api.openai.com/v1/audio/transcriptions";
+const MAX_FILE_SIZE = 25 * 1024 * 1024; // 25MB - Whisper's limit
+
+function getExtensionFromMimeType(mimeType: string): string {
+ const subtype = mimeType.split("/")[1]?.split(";")[0];
+ return subtype || "webm";
+}
+
+export async function POST(request: NextRequest) {
+ const token = await getServerAuthToken();
+
+ if (!token || token === "no-token-found") {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const apiKey = process.env.OPENAI_API_KEY;
+
+ if (!apiKey) {
+ return NextResponse.json(
+ { error: "OpenAI API key not configured" },
+ { status: 401 },
+ );
+ }
+
+ try {
+ const formData = await request.formData();
+ const audioFile = formData.get("audio");
+
+ if (!audioFile || !(audioFile instanceof Blob)) {
+ return NextResponse.json(
+ { error: "No audio file provided" },
+ { status: 400 },
+ );
+ }
+
+ if (audioFile.size > MAX_FILE_SIZE) {
+ return NextResponse.json(
+ { error: "File too large. Maximum size is 25MB." },
+ { status: 413 },
+ );
+ }
+
+ const ext = getExtensionFromMimeType(audioFile.type);
+ const whisperFormData = new FormData();
+ whisperFormData.append("file", audioFile, `recording.${ext}`);
+ whisperFormData.append("model", "whisper-1");
+
+ const response = await fetch(WHISPER_API_URL, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ },
+ body: whisperFormData,
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ console.error("Whisper API error:", errorData);
+ return NextResponse.json(
+ { error: errorData.error?.message || "Transcription failed" },
+ { status: response.status },
+ );
+ }
+
+ const result = await response.json();
+ return NextResponse.json({ text: result.text });
+ } catch (error) {
+ console.error("Transcription error:", error);
+ return NextResponse.json(
+ { error: "Failed to process audio" },
+ { status: 500 },
+ );
+ }
+}
diff --git a/autogpt_platform/frontend/src/app/globals.css b/autogpt_platform/frontend/src/app/globals.css
index 1f782f753b..dd1d17cde7 100644
--- a/autogpt_platform/frontend/src/app/globals.css
+++ b/autogpt_platform/frontend/src/app/globals.css
@@ -1,6 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+@source "../node_modules/streamdown/dist/*.js";
@layer base {
:root {
@@ -29,6 +30,14 @@
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
+ --sidebar-background: 0 0% 98%;
+ --sidebar-foreground: 240 5.3% 26.1%;
+ --sidebar-primary: 240 5.9% 10%;
+ --sidebar-primary-foreground: 0 0% 98%;
+ --sidebar-accent: 240 4.8% 95.9%;
+ --sidebar-accent-foreground: 240 5.9% 10%;
+ --sidebar-border: 220 13% 91%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
@@ -56,6 +65,14 @@
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
+ --sidebar-background: 240 5.9% 10%;
+ --sidebar-foreground: 240 4.8% 95.9%;
+ --sidebar-primary: 224.3 76.3% 48%;
+ --sidebar-primary-foreground: 0 0% 100%;
+ --sidebar-accent: 240 3.7% 15.9%;
+ --sidebar-accent-foreground: 240 4.8% 95.9%;
+ --sidebar-border: 240 3.7% 15.9%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
}
* {
diff --git a/autogpt_platform/frontend/src/app/page.tsx b/autogpt_platform/frontend/src/app/page.tsx
index dbfab49469..ce67760eda 100644
--- a/autogpt_platform/frontend/src/app/page.tsx
+++ b/autogpt_platform/frontend/src/app/page.tsx
@@ -1,27 +1,15 @@
"use client";
-import { getHomepageRoute } from "@/lib/constants";
-import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
+import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function Page() {
- const isChatEnabled = useGetFlag(Flag.CHAT);
const router = useRouter();
- const homepageRoute = getHomepageRoute(isChatEnabled);
- const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true";
- const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID;
- const isLaunchDarklyConfigured = envEnabled && Boolean(clientId);
- const isFlagReady =
- !isLaunchDarklyConfigured || typeof isChatEnabled === "boolean";
- useEffect(
- function redirectToHomepage() {
- if (!isFlagReady) return;
- router.replace(homepageRoute);
- },
- [homepageRoute, isFlagReady, router],
- );
+ useEffect(() => {
+ router.replace("/copilot");
+ }, [router]);
- return null;
+ return ;
}
diff --git a/autogpt_platform/frontend/src/components/__legacy__/ui/render.tsx b/autogpt_platform/frontend/src/components/__legacy__/ui/render.tsx
index 5173326f23..b290c51809 100644
--- a/autogpt_platform/frontend/src/components/__legacy__/ui/render.tsx
+++ b/autogpt_platform/frontend/src/components/__legacy__/ui/render.tsx
@@ -22,7 +22,7 @@ const isValidVideoUrl = (url: string): boolean => {
if (url.startsWith("data:video")) {
return true;
}
- const videoExtensions = /\.(mp4|webm|ogg)$/i;
+ const videoExtensions = /\.(mp4|webm|ogg|mov|avi|mkv|m4v)$/i;
const youtubeRegex = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/;
const cleanedUrl = url.split("?")[0];
return (
@@ -44,11 +44,29 @@ const isValidAudioUrl = (url: string): boolean => {
if (url.startsWith("data:audio")) {
return true;
}
- const audioExtensions = /\.(mp3|wav)$/i;
+ const audioExtensions = /\.(mp3|wav|ogg|m4a|aac|flac)$/i;
const cleanedUrl = url.split("?")[0];
return isValidMediaUri(url) && audioExtensions.test(cleanedUrl);
};
+const getVideoMimeType = (url: string): string => {
+ if (url.startsWith("data:video/")) {
+ const match = url.match(/^data:(video\/[^;]+)/);
+ return match?.[1] || "video/mp4";
+ }
+ const extension = url.split("?")[0].split(".").pop()?.toLowerCase();
+ const mimeMap: Record = {
+ mp4: "video/mp4",
+ webm: "video/webm",
+ ogg: "video/ogg",
+ mov: "video/quicktime",
+ avi: "video/x-msvideo",
+ mkv: "video/x-matroska",
+ m4v: "video/mp4",
+ };
+ return mimeMap[extension || ""] || "video/mp4";
+};
+
const VideoRenderer: React.FC<{ videoUrl: string }> = ({ videoUrl }) => {
const videoId = getYouTubeVideoId(videoUrl);
return (
@@ -63,7 +81,7 @@ const VideoRenderer: React.FC<{ videoUrl: string }> = ({ videoUrl }) => {
>
) : (
)}
diff --git a/autogpt_platform/frontend/src/components/ai-elements/conversation.tsx b/autogpt_platform/frontend/src/components/ai-elements/conversation.tsx
new file mode 100644
index 0000000000..92e940c715
--- /dev/null
+++ b/autogpt_platform/frontend/src/components/ai-elements/conversation.tsx
@@ -0,0 +1,109 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import { scrollbarStyles } from "@/components/styles/scrollbars";
+import { cn } from "@/lib/utils";
+import { ArrowDownIcon } from "lucide-react";
+import type { ComponentProps } from "react";
+import { useCallback } from "react";
+import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
+
+export type ConversationProps = ComponentProps;
+
+export const Conversation = ({ className, ...props }: ConversationProps) => (
+
+);
+
+export type ConversationContentProps = ComponentProps<
+ typeof StickToBottom.Content
+>;
+
+export const ConversationContent = ({
+ className,
+ ...props
+}: ConversationContentProps) => (
+
+);
+
+export type ConversationEmptyStateProps = ComponentProps<"div"> & {
+ title?: string;
+ description?: string;
+ icon?: React.ReactNode;
+};
+
+export const ConversationEmptyState = ({
+ className,
+ title = "No messages yet",
+ description = "Start a conversation to see messages here",
+ icon,
+ children,
+ ...props
+}: ConversationEmptyStateProps) => (
+
+ {children ?? (
+ <>
+ {icon && (
+
{icon}
+ )}
+
+
{title}
+ {description && (
+
+ {description}
+
+ )}
+
+ >
+ )}
+
+);
+
+export type ConversationScrollButtonProps = ComponentProps;
+
+export const ConversationScrollButton = ({
+ className,
+ ...props
+}: ConversationScrollButtonProps) => {
+ const { isAtBottom, scrollToBottom } = useStickToBottomContext();
+
+ const handleScrollToBottom = useCallback(() => {
+ scrollToBottom();
+ }, [scrollToBottom]);
+
+ return (
+ !isAtBottom && (
+
+ )
+ );
+};
diff --git a/autogpt_platform/frontend/src/components/ai-elements/message.tsx b/autogpt_platform/frontend/src/components/ai-elements/message.tsx
new file mode 100644
index 0000000000..5cc330e57c
--- /dev/null
+++ b/autogpt_platform/frontend/src/components/ai-elements/message.tsx
@@ -0,0 +1,338 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { cn } from "@/lib/utils";
+import { cjk } from "@streamdown/cjk";
+import { code } from "@streamdown/code";
+import { math } from "@streamdown/math";
+import { mermaid } from "@streamdown/mermaid";
+import type { UIMessage } from "ai";
+import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
+import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
+import { createContext, memo, useContext, useEffect, useState } from "react";
+import { Streamdown } from "streamdown";
+
+export type MessageProps = HTMLAttributes & {
+ from: UIMessage["role"];
+};
+
+export const Message = ({ className, from, ...props }: MessageProps) => (
+
+);
+
+export type MessageContentProps = HTMLAttributes;
+
+export const MessageContent = ({
+ children,
+ className,
+ ...props
+}: MessageContentProps) => (
+
+ {children}
+
+);
+
+export type MessageActionsProps = ComponentProps<"div">;
+
+export const MessageActions = ({
+ className,
+ children,
+ ...props
+}: MessageActionsProps) => (
+
+ {children}
+
+);
+
+export type MessageActionProps = ComponentProps & {
+ tooltip?: string;
+ label?: string;
+};
+
+export const MessageAction = ({
+ tooltip,
+ children,
+ label,
+ variant = "ghost",
+ size = "icon-sm",
+ ...props
+}: MessageActionProps) => {
+ const button = (
+
+ );
+
+ if (tooltip) {
+ return (
+
+
+ {button}
+
+ {tooltip}
+
+
+
+ );
+ }
+
+ return button;
+};
+
+interface MessageBranchContextType {
+ currentBranch: number;
+ totalBranches: number;
+ goToPrevious: () => void;
+ goToNext: () => void;
+ branches: ReactElement[];
+ setBranches: (branches: ReactElement[]) => void;
+}
+
+const MessageBranchContext = createContext(
+ null,
+);
+
+const useMessageBranch = () => {
+ const context = useContext(MessageBranchContext);
+
+ if (!context) {
+ throw new Error("MessageBranch components must be used within");
+ }
+
+ return context;
+};
+
+export type MessageBranchProps = HTMLAttributes & {
+ defaultBranch?: number;
+ onBranchChange?: (branchIndex: number) => void;
+};
+
+export const MessageBranch = ({
+ defaultBranch = 0,
+ onBranchChange,
+ className,
+ ...props
+}: MessageBranchProps) => {
+ const [currentBranch, setCurrentBranch] = useState(defaultBranch);
+ const [branches, setBranches] = useState([]);
+
+ const handleBranchChange = (newBranch: number) => {
+ setCurrentBranch(newBranch);
+ onBranchChange?.(newBranch);
+ };
+
+ const goToPrevious = () => {
+ const newBranch =
+ currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
+ handleBranchChange(newBranch);
+ };
+
+ const goToNext = () => {
+ const newBranch =
+ currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
+ handleBranchChange(newBranch);
+ };
+
+ const contextValue: MessageBranchContextType = {
+ currentBranch,
+ totalBranches: branches.length,
+ goToPrevious,
+ goToNext,
+ branches,
+ setBranches,
+ };
+
+ return (
+
+ div]:pb-0", className)}
+ {...props}
+ />
+
+ );
+};
+
+export type MessageBranchContentProps = HTMLAttributes
;
+
+export const MessageBranchContent = ({
+ children,
+ ...props
+}: MessageBranchContentProps) => {
+ const { currentBranch, setBranches, branches } = useMessageBranch();
+ const childrenArray = Array.isArray(children) ? children : [children];
+
+ // Use useEffect to update branches when they change
+ useEffect(() => {
+ if (branches.length !== childrenArray.length) {
+ setBranches(childrenArray);
+ }
+ }, [childrenArray, branches, setBranches]);
+
+ return childrenArray.map((branch, index) => (
+ div]:pb-0",
+ index === currentBranch ? "block" : "hidden",
+ )}
+ key={branch.key}
+ {...props}
+ >
+ {branch}
+
+ ));
+};
+
+export type MessageBranchSelectorProps = HTMLAttributes & {
+ from: UIMessage["role"];
+};
+
+export const MessageBranchSelector = ({
+ className,
+ from: _from,
+ ...props
+}: MessageBranchSelectorProps) => {
+ const { totalBranches } = useMessageBranch();
+
+ // Don't render if there's only one branch
+ if (totalBranches <= 1) {
+ return null;
+ }
+
+ return (
+ *:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md",
+ className,
+ )}
+ orientation="horizontal"
+ {...props}
+ />
+ );
+};
+
+export type MessageBranchPreviousProps = ComponentProps;
+
+export const MessageBranchPrevious = ({
+ children,
+ ...props
+}: MessageBranchPreviousProps) => {
+ const { goToPrevious, totalBranches } = useMessageBranch();
+
+ return (
+
+ );
+};
+
+export type MessageBranchNextProps = ComponentProps;
+
+export const MessageBranchNext = ({
+ children,
+ ...props
+}: MessageBranchNextProps) => {
+ const { goToNext, totalBranches } = useMessageBranch();
+
+ return (
+
+ );
+};
+
+export type MessageBranchPageProps = HTMLAttributes;
+
+export const MessageBranchPage = ({
+ className,
+ ...props
+}: MessageBranchPageProps) => {
+ const { currentBranch, totalBranches } = useMessageBranch();
+
+ return (
+
+ {currentBranch + 1} of {totalBranches}
+
+ );
+};
+
+export type MessageResponseProps = ComponentProps;
+
+export const MessageResponse = memo(
+ ({ className, ...props }: MessageResponseProps) => (
+ *:first-child]:mt-0 [&>*:last-child]:mb-0 [&_pre]:!bg-white",
+ className,
+ )}
+ plugins={{ code, mermaid, math, cjk }}
+ {...props}
+ />
+ ),
+ (prevProps, nextProps) => prevProps.children === nextProps.children,
+);
+
+MessageResponse.displayName = "MessageResponse";
+
+export type MessageToolbarProps = ComponentProps<"div">;
+
+export const MessageToolbar = ({
+ className,
+ children,
+ ...props
+}: MessageToolbarProps) => (
+
+ {children}
+
+);
diff --git a/autogpt_platform/frontend/src/components/atoms/FileInput/FileInput.tsx b/autogpt_platform/frontend/src/components/atoms/FileInput/FileInput.tsx
index d43063b411..2677a7483b 100644
--- a/autogpt_platform/frontend/src/components/atoms/FileInput/FileInput.tsx
+++ b/autogpt_platform/frontend/src/components/atoms/FileInput/FileInput.tsx
@@ -104,7 +104,31 @@ export function FileInput(props: Props) {
return false;
}
- const getFileLabelFromValue = (val: string) => {
+ const getFileLabelFromValue = (val: unknown): string => {
+ // Handle object format from external API: { name, type, size, data }
+ if (val && typeof val === "object") {
+ const obj = val as Record;
+ if (typeof obj.name === "string") {
+ return getFileLabel(
+ obj.name,
+ typeof obj.type === "string" ? obj.type : "",
+ );
+ }
+ if (typeof obj.type === "string") {
+ const mimeParts = obj.type.split("/");
+ if (mimeParts.length > 1) {
+ return `${mimeParts[1].toUpperCase()} file`;
+ }
+ return `${obj.type} file`;
+ }
+ return "File";
+ }
+
+ // Handle string values (data URIs or file paths)
+ if (typeof val !== "string") {
+ return "File";
+ }
+
if (val.startsWith("data:")) {
const matches = val.match(/^data:([^;]+);/);
if (matches?.[1]) {
diff --git a/autogpt_platform/frontend/src/components/atoms/OverflowText/OverflowText.tsx b/autogpt_platform/frontend/src/components/atoms/OverflowText/OverflowText.tsx
index efc345f79c..b118cc5aa0 100644
--- a/autogpt_platform/frontend/src/components/atoms/OverflowText/OverflowText.tsx
+++ b/autogpt_platform/frontend/src/components/atoms/OverflowText/OverflowText.tsx
@@ -77,7 +77,7 @@ export function OverflowText(props: Props) {
"block min-w-0 overflow-hidden text-ellipsis whitespace-nowrap",
)}
>
-
+
{value}
diff --git a/autogpt_platform/frontend/src/components/atoms/Text/Text.tsx b/autogpt_platform/frontend/src/components/atoms/Text/Text.tsx
index 8bae184e5b..86c39b6436 100644
--- a/autogpt_platform/frontend/src/components/atoms/Text/Text.tsx
+++ b/autogpt_platform/frontend/src/components/atoms/Text/Text.tsx
@@ -1,4 +1,5 @@
import React from "react";
+import { cn } from "@/lib/utils";
import { As, Variant, variantElementMap, variants } from "./helpers";
type CustomProps = {
@@ -22,7 +23,7 @@ export function Text({
}: TextProps) {
const variantClasses = variants[size || variant] || variants.body;
const Element = outerAs || variantElementMap[variant];
- const combinedClassName = `${variantClasses} ${className}`.trim();
+ const combinedClassName = cn(variantClasses, className);
return React.createElement(
Element,
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/Chat.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/Chat.tsx
deleted file mode 100644
index ba7584765d..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/Chat.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-"use client";
-
-import { Text } from "@/components/atoms/Text/Text";
-import { cn } from "@/lib/utils";
-import { useEffect, useRef } from "react";
-import { ChatContainer } from "./components/ChatContainer/ChatContainer";
-import { ChatErrorState } from "./components/ChatErrorState/ChatErrorState";
-import { ChatLoader } from "./components/ChatLoader/ChatLoader";
-import { useChat } from "./useChat";
-
-export interface ChatProps {
- className?: string;
- urlSessionId?: string | null;
- initialPrompt?: string;
- onSessionNotFound?: () => void;
- onStreamingChange?: (isStreaming: boolean) => void;
-}
-
-export function Chat({
- className,
- urlSessionId,
- initialPrompt,
- onSessionNotFound,
- onStreamingChange,
-}: ChatProps) {
- const hasHandledNotFoundRef = useRef(false);
- const {
- messages,
- isLoading,
- isCreating,
- error,
- isSessionNotFound,
- sessionId,
- createSession,
- showLoader,
- } = useChat({ urlSessionId });
-
- useEffect(
- function handleMissingSession() {
- if (!onSessionNotFound) return;
- if (!urlSessionId) return;
- if (!isSessionNotFound || isLoading || isCreating) return;
- if (hasHandledNotFoundRef.current) return;
- hasHandledNotFoundRef.current = true;
- onSessionNotFound();
- },
- [onSessionNotFound, urlSessionId, isSessionNotFound, isLoading, isCreating],
- );
-
- return (
-
- {/* Main Content */}
-
- {/* Loading State */}
- {showLoader && (isLoading || isCreating) && (
-
-
-
-
- Loading your chats...
-
-
-
- )}
-
- {/* Error State */}
- {error && !isLoading && (
-
- )}
-
- {/* Session Content */}
- {sessionId && !isLoading && !error && (
-
- )}
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/chat-store.ts b/autogpt_platform/frontend/src/components/contextual/Chat/chat-store.ts
deleted file mode 100644
index 28028369a9..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/chat-store.ts
+++ /dev/null
@@ -1,234 +0,0 @@
-"use client";
-
-import { create } from "zustand";
-import type {
- ActiveStream,
- StreamChunk,
- StreamCompleteCallback,
- StreamResult,
- StreamStatus,
-} from "./chat-types";
-import { executeStream } from "./stream-executor";
-
-const COMPLETED_STREAM_TTL = 5 * 60 * 1000; // 5 minutes
-
-interface ChatStoreState {
- activeStreams: Map;
- completedStreams: Map;
- activeSessions: Set;
- streamCompleteCallbacks: Set;
-}
-
-interface ChatStoreActions {
- startStream: (
- sessionId: string,
- message: string,
- isUserMessage: boolean,
- context?: { url: string; content: string },
- onChunk?: (chunk: StreamChunk) => void,
- ) => Promise;
- stopStream: (sessionId: string) => void;
- subscribeToStream: (
- sessionId: string,
- onChunk: (chunk: StreamChunk) => void,
- skipReplay?: boolean,
- ) => () => void;
- getStreamStatus: (sessionId: string) => StreamStatus;
- getCompletedStream: (sessionId: string) => StreamResult | undefined;
- clearCompletedStream: (sessionId: string) => void;
- isStreaming: (sessionId: string) => boolean;
- registerActiveSession: (sessionId: string) => void;
- unregisterActiveSession: (sessionId: string) => void;
- isSessionActive: (sessionId: string) => boolean;
- onStreamComplete: (callback: StreamCompleteCallback) => () => void;
-}
-
-type ChatStore = ChatStoreState & ChatStoreActions;
-
-function notifyStreamComplete(
- callbacks: Set,
- sessionId: string,
-) {
- for (const callback of callbacks) {
- try {
- callback(sessionId);
- } catch (err) {
- console.warn("[ChatStore] Stream complete callback error:", err);
- }
- }
-}
-
-function cleanupCompletedStreams(completedStreams: Map) {
- const now = Date.now();
- for (const [sessionId, result] of completedStreams) {
- if (now - result.completedAt > COMPLETED_STREAM_TTL) {
- completedStreams.delete(sessionId);
- }
- }
-}
-
-function moveToCompleted(
- activeStreams: Map,
- completedStreams: Map,
- streamCompleteCallbacks: Set,
- sessionId: string,
-) {
- const stream = activeStreams.get(sessionId);
- if (!stream) return;
-
- const result: StreamResult = {
- sessionId,
- status: stream.status,
- chunks: stream.chunks,
- completedAt: Date.now(),
- error: stream.error,
- };
-
- completedStreams.set(sessionId, result);
- activeStreams.delete(sessionId);
- cleanupCompletedStreams(completedStreams);
-
- if (stream.status === "completed" || stream.status === "error") {
- notifyStreamComplete(streamCompleteCallbacks, sessionId);
- }
-}
-
-export const useChatStore = create((set, get) => ({
- activeStreams: new Map(),
- completedStreams: new Map(),
- activeSessions: new Set(),
- streamCompleteCallbacks: new Set(),
-
- startStream: async function startStream(
- sessionId,
- message,
- isUserMessage,
- context,
- onChunk,
- ) {
- const { activeStreams, completedStreams, streamCompleteCallbacks } = get();
-
- const existingStream = activeStreams.get(sessionId);
- if (existingStream) {
- existingStream.abortController.abort();
- moveToCompleted(
- activeStreams,
- completedStreams,
- streamCompleteCallbacks,
- sessionId,
- );
- }
-
- const abortController = new AbortController();
- const initialCallbacks = new Set<(chunk: StreamChunk) => void>();
- if (onChunk) initialCallbacks.add(onChunk);
-
- const stream: ActiveStream = {
- sessionId,
- abortController,
- status: "streaming",
- startedAt: Date.now(),
- chunks: [],
- onChunkCallbacks: initialCallbacks,
- };
-
- activeStreams.set(sessionId, stream);
-
- try {
- await executeStream(stream, message, isUserMessage, context);
- } finally {
- if (onChunk) stream.onChunkCallbacks.delete(onChunk);
- if (stream.status !== "streaming") {
- moveToCompleted(
- activeStreams,
- completedStreams,
- streamCompleteCallbacks,
- sessionId,
- );
- }
- }
- },
-
- stopStream: function stopStream(sessionId) {
- const { activeStreams, completedStreams, streamCompleteCallbacks } = get();
- const stream = activeStreams.get(sessionId);
- if (stream) {
- stream.abortController.abort();
- stream.status = "completed";
- moveToCompleted(
- activeStreams,
- completedStreams,
- streamCompleteCallbacks,
- sessionId,
- );
- }
- },
-
- subscribeToStream: function subscribeToStream(
- sessionId,
- onChunk,
- skipReplay = false,
- ) {
- const { activeStreams } = get();
-
- const stream = activeStreams.get(sessionId);
- if (stream) {
- if (!skipReplay) {
- for (const chunk of stream.chunks) {
- onChunk(chunk);
- }
- }
- stream.onChunkCallbacks.add(onChunk);
- return function unsubscribe() {
- stream.onChunkCallbacks.delete(onChunk);
- };
- }
-
- return function noop() {};
- },
-
- getStreamStatus: function getStreamStatus(sessionId) {
- const { activeStreams, completedStreams } = get();
-
- const active = activeStreams.get(sessionId);
- if (active) return active.status;
-
- const completed = completedStreams.get(sessionId);
- if (completed) return completed.status;
-
- return "idle";
- },
-
- getCompletedStream: function getCompletedStream(sessionId) {
- return get().completedStreams.get(sessionId);
- },
-
- clearCompletedStream: function clearCompletedStream(sessionId) {
- get().completedStreams.delete(sessionId);
- },
-
- isStreaming: function isStreaming(sessionId) {
- const stream = get().activeStreams.get(sessionId);
- return stream?.status === "streaming";
- },
-
- registerActiveSession: function registerActiveSession(sessionId) {
- get().activeSessions.add(sessionId);
- },
-
- unregisterActiveSession: function unregisterActiveSession(sessionId) {
- get().activeSessions.delete(sessionId);
- },
-
- isSessionActive: function isSessionActive(sessionId) {
- return get().activeSessions.has(sessionId);
- },
-
- onStreamComplete: function onStreamComplete(callback) {
- const { streamCompleteCallbacks } = get();
- streamCompleteCallbacks.add(callback);
- return function unsubscribe() {
- streamCompleteCallbacks.delete(callback);
- };
- },
-}));
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/chat-types.ts b/autogpt_platform/frontend/src/components/contextual/Chat/chat-types.ts
deleted file mode 100644
index 8c8aa7b704..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/chat-types.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import type { ToolArguments, ToolResult } from "@/types/chat";
-
-export type StreamStatus = "idle" | "streaming" | "completed" | "error";
-
-export interface StreamChunk {
- type:
- | "text_chunk"
- | "text_ended"
- | "tool_call"
- | "tool_call_start"
- | "tool_response"
- | "login_needed"
- | "need_login"
- | "credentials_needed"
- | "error"
- | "usage"
- | "stream_end";
- timestamp?: string;
- content?: string;
- message?: string;
- code?: string;
- details?: Record;
- tool_id?: string;
- tool_name?: string;
- arguments?: ToolArguments;
- result?: ToolResult;
- success?: boolean;
- idx?: number;
- session_id?: string;
- agent_info?: {
- graph_id: string;
- name: string;
- trigger_type: string;
- };
- provider?: string;
- provider_name?: string;
- credential_type?: string;
- scopes?: string[];
- title?: string;
- [key: string]: unknown;
-}
-
-export type VercelStreamChunk =
- | { type: "start"; messageId: string }
- | { type: "finish" }
- | { type: "text-start"; id: string }
- | { type: "text-delta"; id: string; delta: string }
- | { type: "text-end"; id: string }
- | { type: "tool-input-start"; toolCallId: string; toolName: string }
- | {
- type: "tool-input-available";
- toolCallId: string;
- toolName: string;
- input: Record;
- }
- | {
- type: "tool-output-available";
- toolCallId: string;
- toolName?: string;
- output: unknown;
- success?: boolean;
- }
- | {
- type: "usage";
- promptTokens: number;
- completionTokens: number;
- totalTokens: number;
- }
- | {
- type: "error";
- errorText: string;
- code?: string;
- details?: Record;
- };
-
-export interface ActiveStream {
- sessionId: string;
- abortController: AbortController;
- status: StreamStatus;
- startedAt: number;
- chunks: StreamChunk[];
- error?: Error;
- onChunkCallbacks: Set<(chunk: StreamChunk) => void>;
-}
-
-export interface StreamResult {
- sessionId: string;
- status: StreamStatus;
- chunks: StreamChunk[];
- completedAt: number;
- error?: Error;
-}
-
-export type StreamCompleteCallback = (sessionId: string) => void;
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/AIChatBubble/AIChatBubble.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/AIChatBubble/AIChatBubble.tsx
deleted file mode 100644
index f5d56fcb15..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/AIChatBubble/AIChatBubble.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { cn } from "@/lib/utils";
-import { ReactNode } from "react";
-
-export interface AIChatBubbleProps {
- children: ReactNode;
- className?: string;
-}
-
-export function AIChatBubble({ children, className }: AIChatBubbleProps) {
- return (
-
- {children}
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/AgentCarouselMessage/AgentCarouselMessage.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/AgentCarouselMessage/AgentCarouselMessage.tsx
deleted file mode 100644
index 582b24de5e..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/AgentCarouselMessage/AgentCarouselMessage.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-import { Button } from "@/components/atoms/Button/Button";
-import { Card } from "@/components/atoms/Card/Card";
-import { Text } from "@/components/atoms/Text/Text";
-import { cn } from "@/lib/utils";
-import { ArrowRight, List, Robot } from "@phosphor-icons/react";
-import Image from "next/image";
-
-export interface Agent {
- id: string;
- name: string;
- description: string;
- version?: number;
- image_url?: string;
-}
-
-export interface AgentCarouselMessageProps {
- agents: Agent[];
- totalCount?: number;
- onSelectAgent?: (agentId: string) => void;
- className?: string;
-}
-
-export function AgentCarouselMessage({
- agents,
- totalCount,
- onSelectAgent,
- className,
-}: AgentCarouselMessageProps) {
- const displayCount = totalCount ?? agents.length;
-
- return (
-
- {/* Header */}
-
-
-
-
-
-
- Found {displayCount} {displayCount === 1 ? "Agent" : "Agents"}
-
-
- Select an agent to view details or run it
-
-
-
-
- {/* Agent Cards */}
-
- {agents.map((agent) => (
-
-
-
- {agent.image_url ? (
-
- ) : (
-
-
-
- )}
-
-
-
-
- {agent.name}
-
- {agent.version && (
-
- v{agent.version}
-
- )}
-
-
- {agent.description}
-
- {onSelectAgent && (
-
- )}
-
-
-
- ))}
-
-
- {totalCount && totalCount > agents.length && (
-
- Showing {agents.length} of {totalCount} results
-
- )}
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/AgentInputsSetup/AgentInputsSetup.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/AgentInputsSetup/AgentInputsSetup.tsx
deleted file mode 100644
index 3ef71eca09..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/AgentInputsSetup/AgentInputsSetup.tsx
+++ /dev/null
@@ -1,246 +0,0 @@
-"use client";
-
-import { Button } from "@/components/atoms/Button/Button";
-import { Card } from "@/components/atoms/Card/Card";
-import { Text } from "@/components/atoms/Text/Text";
-import { CredentialsInput } from "@/components/contextual/CredentialsInput/CredentialsInput";
-import { RunAgentInputs } from "@/components/contextual/RunAgentInputs/RunAgentInputs";
-
-import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
-import {
- BlockIOCredentialsSubSchema,
- BlockIOSubSchema,
-} from "@/lib/autogpt-server-api/types";
-import { cn, isEmpty } from "@/lib/utils";
-import { PlayIcon, WarningIcon } from "@phosphor-icons/react";
-import { useMemo } from "react";
-import { useAgentInputsSetup } from "./useAgentInputsSetup";
-
-type LibraryAgentInputSchemaProperties = LibraryAgent["input_schema"] extends {
- properties: infer P;
-}
- ? P extends Record
- ? P
- : Record
- : Record;
-
-type LibraryAgentCredentialsInputSchemaProperties =
- LibraryAgent["credentials_input_schema"] extends {
- properties: infer P;
- }
- ? P extends Record
- ? P
- : Record
- : Record;
-
-interface Props {
- agentName?: string;
- inputSchema: LibraryAgentInputSchemaProperties | Record;
- credentialsSchema?:
- | LibraryAgentCredentialsInputSchemaProperties
- | Record;
- message: string;
- requiredFields?: string[];
- onRun: (
- inputs: Record,
- credentials: Record,
- ) => void;
- onCancel?: () => void;
- className?: string;
-}
-
-export function AgentInputsSetup({
- agentName,
- inputSchema,
- credentialsSchema,
- message,
- requiredFields,
- onRun,
- onCancel,
- className,
-}: Props) {
- const { inputValues, setInputValue, credentialsValues, setCredentialsValue } =
- useAgentInputsSetup();
-
- const inputSchemaObj = useMemo(() => {
- if (!inputSchema) return { properties: {}, required: [] };
- if ("properties" in inputSchema && "type" in inputSchema) {
- return inputSchema as {
- properties: Record;
- required?: string[];
- };
- }
- return { properties: inputSchema as Record, required: [] };
- }, [inputSchema]);
-
- const credentialsSchemaObj = useMemo(() => {
- if (!credentialsSchema) return { properties: {}, required: [] };
- if ("properties" in credentialsSchema && "type" in credentialsSchema) {
- return credentialsSchema as {
- properties: Record;
- required?: string[];
- };
- }
- return {
- properties: credentialsSchema as Record,
- required: [],
- };
- }, [credentialsSchema]);
-
- const agentInputFields = useMemo(() => {
- const properties = inputSchemaObj.properties || {};
- return Object.fromEntries(
- Object.entries(properties).filter(
- ([_, subSchema]: [string, any]) => !subSchema.hidden,
- ),
- );
- }, [inputSchemaObj]);
-
- const agentCredentialsInputFields = useMemo(() => {
- return credentialsSchemaObj.properties || {};
- }, [credentialsSchemaObj]);
-
- const inputFields = Object.entries(agentInputFields);
- const credentialFields = Object.entries(agentCredentialsInputFields);
-
- const defaultsFromSchema = useMemo(() => {
- const defaults: Record = {};
- Object.entries(agentInputFields).forEach(([key, schema]) => {
- if ("default" in schema && schema.default !== undefined) {
- defaults[key] = schema.default;
- }
- });
- return defaults;
- }, [agentInputFields]);
-
- const defaultsFromCredentialsSchema = useMemo(() => {
- const defaults: Record = {};
- Object.entries(agentCredentialsInputFields).forEach(([key, schema]) => {
- if ("default" in schema && schema.default !== undefined) {
- defaults[key] = schema.default;
- }
- });
- return defaults;
- }, [agentCredentialsInputFields]);
-
- const mergedInputValues = useMemo(() => {
- return { ...defaultsFromSchema, ...inputValues };
- }, [defaultsFromSchema, inputValues]);
-
- const mergedCredentialsValues = useMemo(() => {
- return { ...defaultsFromCredentialsSchema, ...credentialsValues };
- }, [defaultsFromCredentialsSchema, credentialsValues]);
-
- const allRequiredInputsAreSet = useMemo(() => {
- const requiredInputs = new Set(
- requiredFields || (inputSchemaObj.required as string[]) || [],
- );
- const nonEmptyInputs = new Set(
- Object.keys(mergedInputValues).filter(
- (k) => !isEmpty(mergedInputValues[k]),
- ),
- );
- const missing = [...requiredInputs].filter(
- (input) => !nonEmptyInputs.has(input),
- );
- return missing.length === 0;
- }, [inputSchemaObj.required, mergedInputValues, requiredFields]);
-
- const allCredentialsAreSet = useMemo(() => {
- const requiredCredentials = new Set(
- (credentialsSchemaObj.required as string[]) || [],
- );
- if (requiredCredentials.size === 0) {
- return true;
- }
- const missing = [...requiredCredentials].filter((key) => {
- const cred = mergedCredentialsValues[key];
- return !cred || !cred.id;
- });
- return missing.length === 0;
- }, [credentialsSchemaObj.required, mergedCredentialsValues]);
-
- const canRun = allRequiredInputsAreSet && allCredentialsAreSet;
-
- function handleRun() {
- if (canRun) {
- onRun(mergedInputValues, mergedCredentialsValues);
- }
- }
-
- return (
-
-
-
-
-
-
-
- {agentName ? `Configure ${agentName}` : "Agent Configuration"}
-
-
- {message}
-
-
- {inputFields.length > 0 && (
-
- {inputFields.map(([key, inputSubSchema]) => (
- setInputValue(key, value)}
- />
- ))}
-
- )}
-
- {credentialFields.length > 0 && (
-
- {credentialFields.map(([key, schema]) => {
- const requiredCredentials = new Set(
- (credentialsSchemaObj.required as string[]) || [],
- );
- return (
-
- setCredentialsValue(key, value)
- }
- siblingInputs={mergedInputValues}
- isOptional={!requiredCredentials.has(key)}
- />
- );
- })}
-
- )}
-
-
-
- {onCancel && (
-
- )}
-
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/AgentInputsSetup/useAgentInputsSetup.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/AgentInputsSetup/useAgentInputsSetup.ts
deleted file mode 100644
index e36a3f3c5d..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/AgentInputsSetup/useAgentInputsSetup.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
-import { useState } from "react";
-
-export function useAgentInputsSetup() {
- const [inputValues, setInputValues] = useState>({});
- const [credentialsValues, setCredentialsValues] = useState<
- Record
- >({});
-
- function setInputValue(key: string, value: any) {
- setInputValues((prev) => ({
- ...prev,
- [key]: value,
- }));
- }
-
- function setCredentialsValue(key: string, value?: CredentialsMetaInput) {
- if (value) {
- setCredentialsValues((prev) => ({
- ...prev,
- [key]: value,
- }));
- } else {
- setCredentialsValues((prev) => {
- const next = { ...prev };
- delete next[key];
- return next;
- });
- }
- }
-
- return {
- inputValues,
- setInputValue,
- credentialsValues,
- setCredentialsValue,
- };
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/AuthPromptWidget/AuthPromptWidget.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/AuthPromptWidget/AuthPromptWidget.tsx
deleted file mode 100644
index b2cf92ec56..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/AuthPromptWidget/AuthPromptWidget.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-"use client";
-
-import { Button } from "@/components/atoms/Button/Button";
-import { cn } from "@/lib/utils";
-import { ShieldIcon, SignInIcon, UserPlusIcon } from "@phosphor-icons/react";
-import { useRouter } from "next/navigation";
-
-export interface AuthPromptWidgetProps {
- message: string;
- sessionId: string;
- agentInfo?: {
- graph_id: string;
- name: string;
- trigger_type: string;
- };
- returnUrl?: string;
- className?: string;
-}
-
-export function AuthPromptWidget({
- message,
- sessionId,
- agentInfo,
- returnUrl = "/copilot/chat",
- className,
-}: AuthPromptWidgetProps) {
- const router = useRouter();
-
- function handleSignIn() {
- if (typeof window !== "undefined") {
- localStorage.setItem("pending_chat_session", sessionId);
- if (agentInfo) {
- localStorage.setItem("pending_agent_setup", JSON.stringify(agentInfo));
- }
- }
- const returnUrlWithSession = `${returnUrl}?session_id=${sessionId}`;
- const encodedReturnUrl = encodeURIComponent(returnUrlWithSession);
- router.push(`/login?returnUrl=${encodedReturnUrl}`);
- }
-
- function handleSignUp() {
- if (typeof window !== "undefined") {
- localStorage.setItem("pending_chat_session", sessionId);
- if (agentInfo) {
- localStorage.setItem("pending_agent_setup", JSON.stringify(agentInfo));
- }
- }
- const returnUrlWithSession = `${returnUrl}?session_id=${sessionId}`;
- const encodedReturnUrl = encodeURIComponent(returnUrlWithSession);
- router.push(`/signup?returnUrl=${encodedReturnUrl}`);
- }
-
- return (
-
-
-
-
-
-
-
-
- Authentication Required
-
-
- Sign in to set up and manage agents
-
-
-
-
-
-
{message}
- {agentInfo && (
-
-
- Ready to set up:{" "}
- {agentInfo.name}
-
-
- Type:{" "}
- {agentInfo.trigger_type}
-
-
- )}
-
-
-
-
-
-
-
-
- Your chat session will be preserved after signing in
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/ChatContainer.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/ChatContainer.tsx
deleted file mode 100644
index f062df1397..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/ChatContainer.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse";
-import { Button } from "@/components/atoms/Button/Button";
-import { Text } from "@/components/atoms/Text/Text";
-import { Dialog } from "@/components/molecules/Dialog/Dialog";
-import { useBreakpoint } from "@/lib/hooks/useBreakpoint";
-import { cn } from "@/lib/utils";
-import { GlobeHemisphereEastIcon } from "@phosphor-icons/react";
-import { useEffect } from "react";
-import { ChatInput } from "../ChatInput/ChatInput";
-import { MessageList } from "../MessageList/MessageList";
-import { useChatContainer } from "./useChatContainer";
-
-export interface ChatContainerProps {
- sessionId: string | null;
- initialMessages: SessionDetailResponse["messages"];
- initialPrompt?: string;
- className?: string;
- onStreamingChange?: (isStreaming: boolean) => void;
-}
-
-export function ChatContainer({
- sessionId,
- initialMessages,
- initialPrompt,
- className,
- onStreamingChange,
-}: ChatContainerProps) {
- const {
- messages,
- streamingChunks,
- isStreaming,
- stopStreaming,
- isRegionBlockedModalOpen,
- sendMessageWithContext,
- handleRegionModalOpenChange,
- handleRegionModalClose,
- } = useChatContainer({
- sessionId,
- initialMessages,
- initialPrompt,
- });
-
- useEffect(() => {
- onStreamingChange?.(isStreaming);
- }, [isStreaming, onStreamingChange]);
-
- const breakpoint = useBreakpoint();
- const isMobile =
- breakpoint === "base" || breakpoint === "sm" || breakpoint === "md";
-
- return (
-
-
- }
- controlled={{
- isOpen: isRegionBlockedModalOpen,
- set: handleRegionModalOpenChange,
- }}
- onClose={handleRegionModalClose}
- styling={{ maxWidth: 550, width: "100%", minWidth: "auto" }}
- >
-
-
-
- The Autogpt AI model is not available in your region or your
- connection is blocking it. Please try again with a different
- connection.
-
-
-
-
-
-
-
- {/* Messages - Scrollable */}
-
-
- {/* Input - Fixed at bottom */}
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/createStreamEventDispatcher.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/createStreamEventDispatcher.ts
deleted file mode 100644
index 82e9b05e88..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/createStreamEventDispatcher.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import { toast } from "sonner";
-import type { StreamChunk } from "../../chat-types";
-import type { HandlerDependencies } from "./handlers";
-import {
- handleError,
- handleLoginNeeded,
- handleStreamEnd,
- handleTextChunk,
- handleTextEnded,
- handleToolCallStart,
- handleToolResponse,
- isRegionBlockedError,
-} from "./handlers";
-
-export function createStreamEventDispatcher(
- deps: HandlerDependencies,
-): (chunk: StreamChunk) => void {
- return function dispatchStreamEvent(chunk: StreamChunk): void {
- if (
- chunk.type === "text_chunk" ||
- chunk.type === "tool_call_start" ||
- chunk.type === "tool_response" ||
- chunk.type === "login_needed" ||
- chunk.type === "need_login" ||
- chunk.type === "error"
- ) {
- if (!deps.hasResponseRef.current) {
- console.info("[ChatStream] First response chunk:", {
- type: chunk.type,
- sessionId: deps.sessionId,
- });
- }
- deps.hasResponseRef.current = true;
- }
-
- switch (chunk.type) {
- case "text_chunk":
- handleTextChunk(chunk, deps);
- break;
-
- case "text_ended":
- handleTextEnded(chunk, deps);
- break;
-
- case "tool_call_start":
- handleToolCallStart(chunk, deps);
- break;
-
- case "tool_response":
- handleToolResponse(chunk, deps);
- break;
-
- case "login_needed":
- case "need_login":
- handleLoginNeeded(chunk, deps);
- break;
-
- case "stream_end":
- console.info("[ChatStream] Stream ended:", {
- sessionId: deps.sessionId,
- hasResponse: deps.hasResponseRef.current,
- chunkCount: deps.streamingChunksRef.current.length,
- });
- handleStreamEnd(chunk, deps);
- break;
-
- case "error":
- const isRegionBlocked = isRegionBlockedError(chunk);
- handleError(chunk, deps);
- // Show toast at dispatcher level to avoid circular dependencies
- if (!isRegionBlocked) {
- toast.error("Chat Error", {
- description: chunk.message || chunk.content || "An error occurred",
- });
- }
- break;
-
- case "usage":
- // TODO: Handle usage for display
- break;
-
- default:
- console.warn("Unknown stream chunk type:", chunk);
- }
- };
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/handlers.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/handlers.ts
deleted file mode 100644
index 96198a0386..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/handlers.ts
+++ /dev/null
@@ -1,230 +0,0 @@
-import type { Dispatch, MutableRefObject, SetStateAction } from "react";
-import { StreamChunk } from "../../useChatStream";
-import type { ChatMessageData } from "../ChatMessage/useChatMessage";
-import {
- extractCredentialsNeeded,
- extractInputsNeeded,
- parseToolResponse,
-} from "./helpers";
-
-function isToolCallMessage(
- message: ChatMessageData,
-): message is Extract {
- return message.type === "tool_call";
-}
-
-export interface HandlerDependencies {
- setHasTextChunks: Dispatch>;
- setStreamingChunks: Dispatch>;
- streamingChunksRef: MutableRefObject;
- hasResponseRef: MutableRefObject;
- setMessages: Dispatch>;
- setIsStreamingInitiated: Dispatch>;
- setIsRegionBlockedModalOpen: Dispatch>;
- sessionId: string;
-}
-
-export function isRegionBlockedError(chunk: StreamChunk): boolean {
- if (chunk.code === "MODEL_NOT_AVAILABLE_REGION") return true;
- const message = chunk.message || chunk.content;
- if (typeof message !== "string") return false;
- return message.toLowerCase().includes("not available in your region");
-}
-
-export function handleTextChunk(chunk: StreamChunk, deps: HandlerDependencies) {
- if (!chunk.content) return;
- deps.setHasTextChunks(true);
- deps.setStreamingChunks((prev) => {
- const updated = [...prev, chunk.content!];
- deps.streamingChunksRef.current = updated;
- return updated;
- });
-}
-
-export function handleTextEnded(
- _chunk: StreamChunk,
- deps: HandlerDependencies,
-) {
- const completedText = deps.streamingChunksRef.current.join("");
- if (completedText.trim()) {
- deps.setMessages((prev) => {
- const assistantMessage: ChatMessageData = {
- type: "message",
- role: "assistant",
- content: completedText,
- timestamp: new Date(),
- };
- return [...prev, assistantMessage];
- });
- }
- deps.setStreamingChunks([]);
- deps.streamingChunksRef.current = [];
- deps.setHasTextChunks(false);
-}
-
-export function handleToolCallStart(
- chunk: StreamChunk,
- deps: HandlerDependencies,
-) {
- const toolCallMessage: Extract = {
- type: "tool_call",
- toolId: chunk.tool_id || `tool-${Date.now()}-${chunk.idx || 0}`,
- toolName: chunk.tool_name || "Executing",
- arguments: chunk.arguments || {},
- timestamp: new Date(),
- };
-
- function updateToolCallMessages(prev: ChatMessageData[]) {
- const existingIndex = prev.findIndex(function findToolCallIndex(msg) {
- return isToolCallMessage(msg) && msg.toolId === toolCallMessage.toolId;
- });
- if (existingIndex === -1) {
- return [...prev, toolCallMessage];
- }
- const nextMessages = [...prev];
- const existing = nextMessages[existingIndex];
- if (!isToolCallMessage(existing)) return prev;
- const nextArguments =
- toolCallMessage.arguments &&
- Object.keys(toolCallMessage.arguments).length > 0
- ? toolCallMessage.arguments
- : existing.arguments;
- nextMessages[existingIndex] = {
- ...existing,
- toolName: toolCallMessage.toolName || existing.toolName,
- arguments: nextArguments,
- timestamp: toolCallMessage.timestamp,
- };
- return nextMessages;
- }
-
- deps.setMessages(updateToolCallMessages);
-}
-
-export function handleToolResponse(
- chunk: StreamChunk,
- deps: HandlerDependencies,
-) {
- let toolName = chunk.tool_name || "unknown";
- if (!chunk.tool_name || chunk.tool_name === "unknown") {
- deps.setMessages((prev) => {
- const matchingToolCall = [...prev]
- .reverse()
- .find(
- (msg) => msg.type === "tool_call" && msg.toolId === chunk.tool_id,
- );
- if (matchingToolCall && matchingToolCall.type === "tool_call") {
- toolName = matchingToolCall.toolName;
- }
- return prev;
- });
- }
- const responseMessage = parseToolResponse(
- chunk.result!,
- chunk.tool_id!,
- toolName,
- new Date(),
- );
- if (!responseMessage) {
- let parsedResult: Record | null = null;
- try {
- parsedResult =
- typeof chunk.result === "string"
- ? JSON.parse(chunk.result)
- : (chunk.result as Record);
- } catch {
- parsedResult = null;
- }
- if (
- (chunk.tool_name === "run_agent" || chunk.tool_name === "run_block") &&
- chunk.success &&
- parsedResult?.type === "setup_requirements"
- ) {
- const inputsMessage = extractInputsNeeded(parsedResult, chunk.tool_name);
- if (inputsMessage) {
- deps.setMessages((prev) => [...prev, inputsMessage]);
- }
- const credentialsMessage = extractCredentialsNeeded(
- parsedResult,
- chunk.tool_name,
- );
- if (credentialsMessage) {
- deps.setMessages((prev) => [...prev, credentialsMessage]);
- }
- }
- return;
- }
- deps.setMessages((prev) => {
- const toolCallIndex = prev.findIndex(
- (msg) => msg.type === "tool_call" && msg.toolId === chunk.tool_id,
- );
- const hasResponse = prev.some(
- (msg) => msg.type === "tool_response" && msg.toolId === chunk.tool_id,
- );
- if (hasResponse) return prev;
- if (toolCallIndex !== -1) {
- const newMessages = [...prev];
- newMessages.splice(toolCallIndex + 1, 0, responseMessage);
- return newMessages;
- }
- return [...prev, responseMessage];
- });
-}
-
-export function handleLoginNeeded(
- chunk: StreamChunk,
- deps: HandlerDependencies,
-) {
- const loginNeededMessage: ChatMessageData = {
- type: "login_needed",
- toolName: "login_needed",
- message: chunk.message || "Please sign in to use chat and agent features",
- sessionId: chunk.session_id || deps.sessionId,
- agentInfo: chunk.agent_info,
- timestamp: new Date(),
- };
- deps.setMessages((prev) => [...prev, loginNeededMessage]);
-}
-
-export function handleStreamEnd(
- _chunk: StreamChunk,
- deps: HandlerDependencies,
-) {
- const completedContent = deps.streamingChunksRef.current.join("");
- if (!completedContent.trim() && !deps.hasResponseRef.current) {
- deps.setMessages((prev) => [
- ...prev,
- {
- type: "message",
- role: "assistant",
- content: "No response received. Please try again.",
- timestamp: new Date(),
- },
- ]);
- }
- if (completedContent.trim()) {
- const assistantMessage: ChatMessageData = {
- type: "message",
- role: "assistant",
- content: completedContent,
- timestamp: new Date(),
- };
- deps.setMessages((prev) => [...prev, assistantMessage]);
- }
- deps.setStreamingChunks([]);
- deps.streamingChunksRef.current = [];
- deps.setHasTextChunks(false);
- deps.setIsStreamingInitiated(false);
-}
-
-export function handleError(chunk: StreamChunk, deps: HandlerDependencies) {
- const errorMessage = chunk.message || chunk.content || "An error occurred";
- console.error("Stream error:", errorMessage);
- if (isRegionBlockedError(chunk)) {
- deps.setIsRegionBlockedModalOpen(true);
- }
- deps.setIsStreamingInitiated(false);
- deps.setHasTextChunks(false);
- deps.setStreamingChunks([]);
- deps.streamingChunksRef.current = [];
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/helpers.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/helpers.ts
deleted file mode 100644
index 7dee924634..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/helpers.ts
+++ /dev/null
@@ -1,569 +0,0 @@
-import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse";
-import { SessionKey, sessionStorage } from "@/services/storage/session-storage";
-import type { ToolResult } from "@/types/chat";
-import type { ChatMessageData } from "../ChatMessage/useChatMessage";
-
-export function processInitialMessages(
- initialMessages: SessionDetailResponse["messages"],
-): ChatMessageData[] {
- const processedMessages: ChatMessageData[] = [];
- const toolCallMap = new Map();
-
- for (const msg of initialMessages) {
- if (!isValidMessage(msg)) {
- console.warn("Invalid message structure from backend:", msg);
- continue;
- }
-
- let content = String(msg.content || "");
- const role = String(msg.role || "assistant").toLowerCase();
- const toolCalls = msg.tool_calls;
- const timestamp = msg.timestamp
- ? new Date(msg.timestamp as string)
- : undefined;
-
- if (role === "user") {
- content = removePageContext(content);
- if (!content.trim()) continue;
- processedMessages.push({
- type: "message",
- role: "user",
- content,
- timestamp,
- });
- continue;
- }
-
- if (role === "assistant") {
- content = content
- .replace(/[\s\S]*?<\/thinking>/gi, "")
- .replace(/[\s\S]*?<\/internal_reasoning>/gi, "")
- .trim();
-
- if (toolCalls && isToolCallArray(toolCalls) && toolCalls.length > 0) {
- for (const toolCall of toolCalls) {
- const toolName = toolCall.function.name;
- const toolId = toolCall.id;
- toolCallMap.set(toolId, toolName);
-
- try {
- const args = JSON.parse(toolCall.function.arguments || "{}");
- processedMessages.push({
- type: "tool_call",
- toolId,
- toolName,
- arguments: args,
- timestamp,
- });
- } catch (err) {
- console.warn("Failed to parse tool call arguments:", err);
- processedMessages.push({
- type: "tool_call",
- toolId,
- toolName,
- arguments: {},
- timestamp,
- });
- }
- }
- if (content.trim()) {
- processedMessages.push({
- type: "message",
- role: "assistant",
- content,
- timestamp,
- });
- }
- } else if (content.trim()) {
- processedMessages.push({
- type: "message",
- role: "assistant",
- content,
- timestamp,
- });
- }
- continue;
- }
-
- if (role === "tool") {
- const toolCallId = (msg.tool_call_id as string) || "";
- const toolName = toolCallMap.get(toolCallId) || "unknown";
- const toolResponse = parseToolResponse(
- content,
- toolCallId,
- toolName,
- timestamp,
- );
- if (toolResponse) {
- processedMessages.push(toolResponse);
- }
- continue;
- }
-
- if (content.trim()) {
- processedMessages.push({
- type: "message",
- role: role as "user" | "assistant" | "system",
- content,
- timestamp,
- });
- }
- }
-
- return processedMessages;
-}
-
-export function hasSentInitialPrompt(sessionId: string): boolean {
- try {
- const sent = JSON.parse(
- sessionStorage.get(SessionKey.CHAT_SENT_INITIAL_PROMPTS) || "{}",
- );
- return sent[sessionId] === true;
- } catch {
- return false;
- }
-}
-
-export function markInitialPromptSent(sessionId: string): void {
- try {
- const sent = JSON.parse(
- sessionStorage.get(SessionKey.CHAT_SENT_INITIAL_PROMPTS) || "{}",
- );
- sent[sessionId] = true;
- sessionStorage.set(
- SessionKey.CHAT_SENT_INITIAL_PROMPTS,
- JSON.stringify(sent),
- );
- } catch {
- // Ignore storage errors
- }
-}
-
-export function removePageContext(content: string): string {
- // Remove "Page URL: ..." pattern at start of line (case insensitive, handles various formats)
- let cleaned = content.replace(/^\s*Page URL:\s*[^\n\r]*/gim, "");
-
- // Find "User Message:" marker at start of line to preserve the actual user message
- const userMessageMatch = cleaned.match(/^\s*User Message:\s*([\s\S]*)$/im);
- if (userMessageMatch) {
- // If we found "User Message:", extract everything after it
- cleaned = userMessageMatch[1];
- } else {
- // If no "User Message:" marker, remove "Page Content:" and everything after it at start of line
- cleaned = cleaned.replace(/^\s*Page Content:[\s\S]*$/gim, "");
- }
-
- // Clean up extra whitespace and newlines
- cleaned = cleaned.replace(/\n\s*\n\s*\n+/g, "\n\n").trim();
- return cleaned;
-}
-
-export function createUserMessage(content: string): ChatMessageData {
- return {
- type: "message",
- role: "user",
- content,
- timestamp: new Date(),
- };
-}
-
-export function filterAuthMessages(
- messages: ChatMessageData[],
-): ChatMessageData[] {
- return messages.filter(
- (msg) => msg.type !== "credentials_needed" && msg.type !== "login_needed",
- );
-}
-
-export function isValidMessage(msg: unknown): msg is Record {
- if (typeof msg !== "object" || msg === null) {
- return false;
- }
- const m = msg as Record;
- if (typeof m.role !== "string") {
- return false;
- }
- if (m.content !== undefined && typeof m.content !== "string") {
- return false;
- }
- return true;
-}
-
-export function isToolCallArray(value: unknown): value is Array<{
- id: string;
- type: string;
- function: { name: string; arguments: string };
-}> {
- if (!Array.isArray(value)) {
- return false;
- }
- return value.every(
- (item) =>
- typeof item === "object" &&
- item !== null &&
- "id" in item &&
- typeof item.id === "string" &&
- "type" in item &&
- typeof item.type === "string" &&
- "function" in item &&
- typeof item.function === "object" &&
- item.function !== null &&
- "name" in item.function &&
- typeof item.function.name === "string" &&
- "arguments" in item.function &&
- typeof item.function.arguments === "string",
- );
-}
-
-export function isAgentArray(value: unknown): value is Array<{
- id: string;
- name: string;
- description: string;
- version?: number;
- image_url?: string;
-}> {
- if (!Array.isArray(value)) {
- return false;
- }
- return value.every(
- (item) =>
- typeof item === "object" &&
- item !== null &&
- "id" in item &&
- typeof item.id === "string" &&
- "name" in item &&
- typeof item.name === "string" &&
- "description" in item &&
- typeof item.description === "string" &&
- (!("version" in item) || typeof item.version === "number") &&
- (!("image_url" in item) || typeof item.image_url === "string"),
- );
-}
-
-export function extractJsonFromErrorMessage(
- message: string,
-): Record | null {
- try {
- const start = message.indexOf("{");
- if (start === -1) {
- return null;
- }
- let depth = 0;
- let end = -1;
- for (let i = start; i < message.length; i++) {
- const ch = message[i];
- if (ch === "{") {
- depth++;
- } else if (ch === "}") {
- depth--;
- if (depth === 0) {
- end = i;
- break;
- }
- }
- }
- if (end === -1) {
- return null;
- }
- const jsonStr = message.slice(start, end + 1);
- return JSON.parse(jsonStr) as Record;
- } catch {
- return null;
- }
-}
-
-export function parseToolResponse(
- result: ToolResult,
- toolId: string,
- toolName: string,
- timestamp?: Date,
-): ChatMessageData | null {
- let parsedResult: Record | null = null;
- try {
- parsedResult =
- typeof result === "string"
- ? JSON.parse(result)
- : (result as Record);
- } catch {
- parsedResult = null;
- }
- if (parsedResult && typeof parsedResult === "object") {
- const responseType = parsedResult.type as string | undefined;
- if (responseType === "no_results") {
- return {
- type: "tool_response",
- toolId,
- toolName,
- result: (parsedResult.message as string) || "No results found",
- success: true,
- timestamp: timestamp || new Date(),
- };
- }
- if (responseType === "agent_carousel") {
- const agentsData = parsedResult.agents;
- if (isAgentArray(agentsData)) {
- return {
- type: "agent_carousel",
- toolName: "agent_carousel",
- agents: agentsData,
- totalCount: parsedResult.total_count as number | undefined,
- timestamp: timestamp || new Date(),
- };
- } else {
- console.warn("Invalid agents array in agent_carousel response");
- }
- }
- if (responseType === "execution_started") {
- return {
- type: "execution_started",
- toolName: "execution_started",
- executionId: (parsedResult.execution_id as string) || "",
- agentName: (parsedResult.graph_name as string) || undefined,
- message: parsedResult.message as string | undefined,
- libraryAgentLink: parsedResult.library_agent_link as string | undefined,
- timestamp: timestamp || new Date(),
- };
- }
- if (responseType === "clarification_needed") {
- return {
- type: "clarification_needed",
- toolName,
- questions:
- (parsedResult.questions as Array<{
- question: string;
- keyword: string;
- example?: string;
- }>) || [],
- message:
- (parsedResult.message as string) ||
- "I need more information to proceed.",
- sessionId: (parsedResult.session_id as string) || "",
- timestamp: timestamp || new Date(),
- };
- }
- if (responseType === "need_login") {
- return {
- type: "login_needed",
- toolName: "login_needed",
- message:
- (parsedResult.message as string) ||
- "Please sign in to use chat and agent features",
- sessionId: (parsedResult.session_id as string) || "",
- agentInfo: parsedResult.agent_info as
- | {
- graph_id: string;
- name: string;
- trigger_type: string;
- }
- | undefined,
- timestamp: timestamp || new Date(),
- };
- }
- if (responseType === "setup_requirements") {
- return null;
- }
- if (responseType === "understanding_updated") {
- return {
- type: "tool_response",
- toolId,
- toolName,
- result: (parsedResult || result) as ToolResult,
- success: true,
- timestamp: timestamp || new Date(),
- };
- }
- }
- return {
- type: "tool_response",
- toolId,
- toolName,
- result: parsedResult ? (parsedResult as ToolResult) : result,
- success: true,
- timestamp: timestamp || new Date(),
- };
-}
-
-export function isUserReadiness(
- value: unknown,
-): value is { missing_credentials?: Record } {
- return (
- typeof value === "object" &&
- value !== null &&
- (!("missing_credentials" in value) ||
- typeof (value as any).missing_credentials === "object")
- );
-}
-
-export function isMissingCredentials(
- value: unknown,
-): value is Record> {
- if (typeof value !== "object" || value === null) {
- return false;
- }
- return Object.values(value).every((v) => typeof v === "object" && v !== null);
-}
-
-export function isSetupInfo(value: unknown): value is {
- user_readiness?: Record;
- agent_name?: string;
-} {
- return (
- typeof value === "object" &&
- value !== null &&
- (!("user_readiness" in value) ||
- typeof (value as any).user_readiness === "object") &&
- (!("agent_name" in value) || typeof (value as any).agent_name === "string")
- );
-}
-
-export function extractCredentialsNeeded(
- parsedResult: Record,
- toolName: string = "run_agent",
-): ChatMessageData | null {
- try {
- const setupInfo = parsedResult?.setup_info as
- | Record
- | undefined;
- const userReadiness = setupInfo?.user_readiness as
- | Record
- | undefined;
- const missingCreds = userReadiness?.missing_credentials as
- | Record>
- | undefined;
- if (missingCreds && Object.keys(missingCreds).length > 0) {
- const agentName = (setupInfo?.agent_name as string) || "this block";
- const credentials = Object.values(missingCreds).map((credInfo) => {
- // Normalize to array at boundary - prefer 'types' array, fall back to single 'type'
- const typesArray = credInfo.types as
- | Array<"api_key" | "oauth2" | "user_password" | "host_scoped">
- | undefined;
- const singleType =
- (credInfo.type as
- | "api_key"
- | "oauth2"
- | "user_password"
- | "host_scoped"
- | undefined) || "api_key";
- const credentialTypes =
- typesArray && typesArray.length > 0 ? typesArray : [singleType];
-
- return {
- provider: (credInfo.provider as string) || "unknown",
- providerName:
- (credInfo.provider_name as string) ||
- (credInfo.provider as string) ||
- "Unknown Provider",
- credentialTypes,
- title:
- (credInfo.title as string) ||
- `${(credInfo.provider_name as string) || (credInfo.provider as string)} credentials`,
- scopes: credInfo.scopes as string[] | undefined,
- };
- });
- return {
- type: "credentials_needed",
- toolName,
- credentials,
- message: `To run ${agentName}, you need to add ${credentials.length === 1 ? "credentials" : `${credentials.length} credentials`}.`,
- agentName,
- timestamp: new Date(),
- };
- }
- return null;
- } catch (err) {
- console.error("Failed to extract credentials from setup info:", err);
- return null;
- }
-}
-
-export function extractInputsNeeded(
- parsedResult: Record,
- toolName: string = "run_agent",
-): ChatMessageData | null {
- try {
- const setupInfo = parsedResult?.setup_info as
- | Record
- | undefined;
- const requirements = setupInfo?.requirements as
- | Record
- | undefined;
- const inputs = requirements?.inputs as
- | Array>
- | undefined;
- const credentials = requirements?.credentials as
- | Array>
- | undefined;
-
- if (!inputs || inputs.length === 0) {
- return null;
- }
-
- const agentName = (setupInfo?.agent_name as string) || "this agent";
- const agentId = parsedResult?.graph_id as string | undefined;
- const graphVersion = parsedResult?.graph_version as number | undefined;
-
- const properties: Record = {};
- const requiredProps: string[] = [];
- inputs.forEach((input) => {
- const name = input.name as string;
- if (name) {
- properties[name] = {
- title: input.name as string,
- description: (input.description as string) || "",
- type: (input.type as string) || "string",
- default: input.default,
- enum: input.options,
- format: input.format,
- };
- if ((input.required as boolean) === true) {
- requiredProps.push(name);
- }
- }
- });
-
- const inputSchema: Record = {
- type: "object",
- properties,
- };
- if (requiredProps.length > 0) {
- inputSchema.required = requiredProps;
- }
-
- const credentialsSchema: Record = {};
- if (credentials && credentials.length > 0) {
- credentials.forEach((cred) => {
- const id = cred.id as string;
- if (id) {
- const credentialTypes = Array.isArray(cred.types)
- ? cred.types
- : [(cred.type as string) || "api_key"];
- credentialsSchema[id] = {
- type: "object",
- properties: {},
- credentials_provider: [cred.provider as string],
- credentials_types: credentialTypes,
- credentials_scopes: cred.scopes as string[] | undefined,
- };
- }
- });
- }
-
- return {
- type: "inputs_needed",
- toolName,
- agentName,
- agentId,
- graphVersion,
- inputSchema,
- credentialsSchema:
- Object.keys(credentialsSchema).length > 0
- ? credentialsSchema
- : undefined,
- message: `Please provide the required inputs to run ${agentName}.`,
- timestamp: new Date(),
- };
- } catch (err) {
- console.error("Failed to extract inputs from setup info:", err);
- return null;
- }
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/useChatContainer.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/useChatContainer.ts
deleted file mode 100644
index b7f9d305dd..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatContainer/useChatContainer.ts
+++ /dev/null
@@ -1,199 +0,0 @@
-import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse";
-import { useEffect, useMemo, useRef, useState } from "react";
-import { useChatStore } from "../../chat-store";
-import { toast } from "sonner";
-import { useChatStream } from "../../useChatStream";
-import { usePageContext } from "../../usePageContext";
-import type { ChatMessageData } from "../ChatMessage/useChatMessage";
-import { createStreamEventDispatcher } from "./createStreamEventDispatcher";
-import {
- createUserMessage,
- filterAuthMessages,
- hasSentInitialPrompt,
- markInitialPromptSent,
- processInitialMessages,
-} from "./helpers";
-
-interface Args {
- sessionId: string | null;
- initialMessages: SessionDetailResponse["messages"];
- initialPrompt?: string;
-}
-
-export function useChatContainer({
- sessionId,
- initialMessages,
- initialPrompt,
-}: Args) {
- const [messages, setMessages] = useState([]);
- const [streamingChunks, setStreamingChunks] = useState([]);
- const [hasTextChunks, setHasTextChunks] = useState(false);
- const [isStreamingInitiated, setIsStreamingInitiated] = useState(false);
- const [isRegionBlockedModalOpen, setIsRegionBlockedModalOpen] =
- useState(false);
- const hasResponseRef = useRef(false);
- const streamingChunksRef = useRef([]);
- const previousSessionIdRef = useRef(null);
- const {
- error,
- sendMessage: sendStreamMessage,
- stopStreaming,
- } = useChatStream();
- const activeStreams = useChatStore((s) => s.activeStreams);
- const subscribeToStream = useChatStore((s) => s.subscribeToStream);
- const isStreaming = isStreamingInitiated || hasTextChunks;
-
- useEffect(
- function handleSessionChange() {
- if (sessionId === previousSessionIdRef.current) return;
-
- const prevSession = previousSessionIdRef.current;
- if (prevSession) {
- stopStreaming(prevSession);
- }
- previousSessionIdRef.current = sessionId;
- setMessages([]);
- setStreamingChunks([]);
- streamingChunksRef.current = [];
- setHasTextChunks(false);
- setIsStreamingInitiated(false);
- hasResponseRef.current = false;
-
- if (!sessionId) return;
-
- const activeStream = activeStreams.get(sessionId);
- if (!activeStream || activeStream.status !== "streaming") return;
-
- const dispatcher = createStreamEventDispatcher({
- setHasTextChunks,
- setStreamingChunks,
- streamingChunksRef,
- hasResponseRef,
- setMessages,
- setIsRegionBlockedModalOpen,
- sessionId,
- setIsStreamingInitiated,
- });
-
- setIsStreamingInitiated(true);
- const skipReplay = initialMessages.length > 0;
- return subscribeToStream(sessionId, dispatcher, skipReplay);
- },
- [sessionId, stopStreaming, activeStreams, subscribeToStream],
- );
-
- const allMessages = useMemo(
- () => [...processInitialMessages(initialMessages), ...messages],
- [initialMessages, messages],
- );
-
- async function sendMessage(
- content: string,
- isUserMessage: boolean = true,
- context?: { url: string; content: string },
- ) {
- if (!sessionId) {
- console.error("[useChatContainer] Cannot send message: no session ID");
- return;
- }
- setIsRegionBlockedModalOpen(false);
- if (isUserMessage) {
- const userMessage = createUserMessage(content);
- setMessages((prev) => [...filterAuthMessages(prev), userMessage]);
- } else {
- setMessages((prev) => filterAuthMessages(prev));
- }
- setStreamingChunks([]);
- streamingChunksRef.current = [];
- setHasTextChunks(false);
- setIsStreamingInitiated(true);
- hasResponseRef.current = false;
-
- const dispatcher = createStreamEventDispatcher({
- setHasTextChunks,
- setStreamingChunks,
- streamingChunksRef,
- hasResponseRef,
- setMessages,
- setIsRegionBlockedModalOpen,
- sessionId,
- setIsStreamingInitiated,
- });
-
- try {
- await sendStreamMessage(
- sessionId,
- content,
- dispatcher,
- isUserMessage,
- context,
- );
- } catch (err) {
- console.error("[useChatContainer] Failed to send message:", err);
- setIsStreamingInitiated(false);
-
- if (err instanceof Error && err.name === "AbortError") return;
-
- const errorMessage =
- err instanceof Error ? err.message : "Failed to send message";
- toast.error("Failed to send message", {
- description: errorMessage,
- });
- }
- }
-
- function handleStopStreaming() {
- stopStreaming();
- setStreamingChunks([]);
- streamingChunksRef.current = [];
- setHasTextChunks(false);
- setIsStreamingInitiated(false);
- }
-
- const { capturePageContext } = usePageContext();
- const sendMessageRef = useRef(sendMessage);
- sendMessageRef.current = sendMessage;
-
- useEffect(
- function handleInitialPrompt() {
- if (!initialPrompt || !sessionId) return;
- if (initialMessages.length > 0) return;
- if (hasSentInitialPrompt(sessionId)) return;
-
- markInitialPromptSent(sessionId);
- const context = capturePageContext();
- sendMessageRef.current(initialPrompt, true, context);
- },
- [initialPrompt, sessionId, initialMessages.length, capturePageContext],
- );
-
- async function sendMessageWithContext(
- content: string,
- isUserMessage: boolean = true,
- ) {
- const context = capturePageContext();
- await sendMessage(content, isUserMessage, context);
- }
-
- function handleRegionModalOpenChange(open: boolean) {
- setIsRegionBlockedModalOpen(open);
- }
-
- function handleRegionModalClose() {
- setIsRegionBlockedModalOpen(false);
- }
-
- return {
- messages: allMessages,
- streamingChunks,
- isStreaming,
- error,
- isRegionBlockedModalOpen,
- setIsRegionBlockedModalOpen,
- sendMessageWithContext,
- handleRegionModalOpenChange,
- handleRegionModalClose,
- sendMessage,
- stopStreaming: handleStopStreaming,
- };
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatCredentialsSetup/ChatCredentialsSetup.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatCredentialsSetup/ChatCredentialsSetup.tsx
deleted file mode 100644
index f0dfadd1f7..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatCredentialsSetup/ChatCredentialsSetup.tsx
+++ /dev/null
@@ -1,151 +0,0 @@
-import { Text } from "@/components/atoms/Text/Text";
-import { CredentialsInput } from "@/components/contextual/CredentialsInput/CredentialsInput";
-import type { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api";
-import { cn } from "@/lib/utils";
-import { CheckIcon, RobotIcon, WarningIcon } from "@phosphor-icons/react";
-import { useEffect, useRef } from "react";
-import { useChatCredentialsSetup } from "./useChatCredentialsSetup";
-
-export interface CredentialInfo {
- provider: string;
- providerName: string;
- credentialTypes: Array<
- "api_key" | "oauth2" | "user_password" | "host_scoped"
- >;
- title: string;
- scopes?: string[];
-}
-
-interface Props {
- credentials: CredentialInfo[];
- agentName?: string;
- message: string;
- onAllCredentialsComplete: () => void;
- onCancel: () => void;
- className?: string;
-}
-
-function createSchemaFromCredentialInfo(
- credential: CredentialInfo,
-): BlockIOCredentialsSubSchema {
- return {
- type: "object",
- properties: {},
- credentials_provider: [credential.provider],
- credentials_types: credential.credentialTypes,
- credentials_scopes: credential.scopes,
- discriminator: undefined,
- discriminator_mapping: undefined,
- discriminator_values: undefined,
- };
-}
-
-export function ChatCredentialsSetup({
- credentials,
- agentName: _agentName,
- message,
- onAllCredentialsComplete,
- onCancel: _onCancel,
-}: Props) {
- const { selectedCredentials, isAllComplete, handleCredentialSelect } =
- useChatCredentialsSetup(credentials);
-
- // Track if we've already called completion to prevent double calls
- const hasCalledCompleteRef = useRef(false);
-
- // Reset the completion flag when credentials change (new credential setup flow)
- useEffect(
- function resetCompletionFlag() {
- hasCalledCompleteRef.current = false;
- },
- [credentials],
- );
-
- // Auto-call completion when all credentials are configured
- useEffect(
- function autoCompleteWhenReady() {
- if (isAllComplete && !hasCalledCompleteRef.current) {
- hasCalledCompleteRef.current = true;
- onAllCredentialsComplete();
- }
- },
- [isAllComplete, onAllCredentialsComplete],
- );
-
- return (
-
-
-
-
-
-
-
-
-
-
- Credentials Required
-
-
- {message}
-
-
-
-
- {credentials.map((cred, index) => {
- const schema = createSchemaFromCredentialInfo(cred);
- const isSelected = !!selectedCredentials[cred.provider];
-
- return (
-
-
- {isSelected ? (
-
- ) : (
-
- )}
-
- {cred.providerName}
-
-
-
-
- handleCredentialSelect(cred.provider, credMeta)
- }
- />
-
- );
- })}
-
-
-
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatCredentialsSetup/useChatCredentialsSetup.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatCredentialsSetup/useChatCredentialsSetup.ts
deleted file mode 100644
index 6b4b26e834..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatCredentialsSetup/useChatCredentialsSetup.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { useState, useMemo } from "react";
-import type { CredentialInfo } from "./ChatCredentialsSetup";
-import type { CredentialsMetaInput } from "@/lib/autogpt-server-api";
-
-export function useChatCredentialsSetup(credentials: CredentialInfo[]) {
- const [selectedCredentials, setSelectedCredentials] = useState<
- Record
- >({});
-
- // Check if all credentials are configured
- const isAllComplete = useMemo(
- function checkAllComplete() {
- if (credentials.length === 0) return false;
- return credentials.every((cred) => selectedCredentials[cred.provider]);
- },
- [credentials, selectedCredentials],
- );
-
- function handleCredentialSelect(
- provider: string,
- credential?: CredentialsMetaInput,
- ) {
- if (credential) {
- setSelectedCredentials((prev) => ({
- ...prev,
- [provider]: credential,
- }));
- }
- }
-
- return {
- selectedCredentials,
- isAllComplete,
- handleCredentialSelect,
- };
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatErrorState/ChatErrorState.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatErrorState/ChatErrorState.tsx
deleted file mode 100644
index bac13d1b0c..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatErrorState/ChatErrorState.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from "react";
-import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
-import { cn } from "@/lib/utils";
-
-export interface ChatErrorStateProps {
- error: Error;
- onRetry?: () => void;
- className?: string;
-}
-
-export function ChatErrorState({
- error,
- onRetry,
- className,
-}: ChatErrorStateProps) {
- return (
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/ChatInput.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/ChatInput.tsx
deleted file mode 100644
index c45e8dc250..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatInput/ChatInput.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-import { Button } from "@/components/atoms/Button/Button";
-import { cn } from "@/lib/utils";
-import { ArrowUpIcon, StopIcon } from "@phosphor-icons/react";
-import { useChatInput } from "./useChatInput";
-
-export interface Props {
- onSend: (message: string) => void;
- disabled?: boolean;
- isStreaming?: boolean;
- onStop?: () => void;
- placeholder?: string;
- className?: string;
-}
-
-export function ChatInput({
- onSend,
- disabled = false,
- isStreaming = false,
- onStop,
- placeholder = "Type your message...",
- className,
-}: Props) {
- const inputId = "chat-input";
- const { value, handleKeyDown, handleSubmit, handleChange, hasMultipleLines } =
- useChatInput({
- onSend,
- disabled: disabled || isStreaming,
- maxRows: 4,
- inputId,
- });
-
- return (
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatLoader/ChatLoader.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatLoader/ChatLoader.tsx
deleted file mode 100644
index 76cee8dbae..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatLoader/ChatLoader.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-export function ChatLoader() {
- return (
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatLoadingState/ChatLoadingState.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatLoadingState/ChatLoadingState.tsx
deleted file mode 100644
index c0cdb33c50..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatLoadingState/ChatLoadingState.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
-import { cn } from "@/lib/utils";
-
-export interface ChatLoadingStateProps {
- message?: string;
- className?: string;
-}
-
-export function ChatLoadingState({ className }: ChatLoadingStateProps) {
- return (
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatMessage/ChatMessage.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatMessage/ChatMessage.tsx
deleted file mode 100644
index 0fee33dbc0..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatMessage/ChatMessage.tsx
+++ /dev/null
@@ -1,399 +0,0 @@
-"use client";
-
-import { Button } from "@/components/atoms/Button/Button";
-import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
-import { cn } from "@/lib/utils";
-import {
- ArrowsClockwiseIcon,
- CheckCircleIcon,
- CheckIcon,
-} from "@phosphor-icons/react";
-import { useRouter } from "next/navigation";
-import { useCallback, useState } from "react";
-import { AgentCarouselMessage } from "../AgentCarouselMessage/AgentCarouselMessage";
-import { AIChatBubble } from "../AIChatBubble/AIChatBubble";
-import { AuthPromptWidget } from "../AuthPromptWidget/AuthPromptWidget";
-import { ChatCredentialsSetup } from "../ChatCredentialsSetup/ChatCredentialsSetup";
-import { ClarificationQuestionsWidget } from "../ClarificationQuestionsWidget/ClarificationQuestionsWidget";
-import { ExecutionStartedMessage } from "../ExecutionStartedMessage/ExecutionStartedMessage";
-import { MarkdownContent } from "../MarkdownContent/MarkdownContent";
-import { NoResultsMessage } from "../NoResultsMessage/NoResultsMessage";
-import { ToolCallMessage } from "../ToolCallMessage/ToolCallMessage";
-import { ToolResponseMessage } from "../ToolResponseMessage/ToolResponseMessage";
-import { UserChatBubble } from "../UserChatBubble/UserChatBubble";
-import { useChatMessage, type ChatMessageData } from "./useChatMessage";
-
-function stripInternalReasoning(content: string): string {
- const cleaned = content.replace(
- /[\s\S]*?<\/internal_reasoning>/gi,
- "",
- );
- return cleaned.replace(/\n{3,}/g, "\n\n").trim();
-}
-
-function getDisplayContent(message: ChatMessageData, isUser: boolean): string {
- if (message.type !== "message") return "";
- if (isUser) return message.content;
- return stripInternalReasoning(message.content);
-}
-
-export interface ChatMessageProps {
- message: ChatMessageData;
- messages?: ChatMessageData[];
- index?: number;
- isStreaming?: boolean;
- className?: string;
- onDismissLogin?: () => void;
- onDismissCredentials?: () => void;
- onSendMessage?: (content: string, isUserMessage?: boolean) => void;
- agentOutput?: ChatMessageData;
- isFinalMessage?: boolean;
-}
-
-export function ChatMessage({
- message,
- messages = [],
- index = -1,
- isStreaming = false,
- className,
- onDismissCredentials,
- onSendMessage,
- agentOutput,
- isFinalMessage = true,
-}: ChatMessageProps) {
- const { user } = useSupabase();
- const router = useRouter();
- const [copied, setCopied] = useState(false);
- const {
- isUser,
- isToolCall,
- isToolResponse,
- isLoginNeeded,
- isCredentialsNeeded,
- isClarificationNeeded,
- } = useChatMessage(message);
- const displayContent = getDisplayContent(message, isUser);
-
- const handleAllCredentialsComplete = useCallback(
- function handleAllCredentialsComplete() {
- // Send a user message that explicitly asks to retry the setup
- // This ensures the LLM calls get_required_setup_info again and proceeds with execution
- if (onSendMessage) {
- onSendMessage(
- "I've configured the required credentials. Please check if everything is ready and proceed with setting up the agent.",
- );
- }
- // Optionally dismiss the credentials prompt
- if (onDismissCredentials) {
- onDismissCredentials();
- }
- },
- [onSendMessage, onDismissCredentials],
- );
-
- function handleCancelCredentials() {
- // Dismiss the credentials prompt
- if (onDismissCredentials) {
- onDismissCredentials();
- }
- }
-
- function handleClarificationAnswers(answers: Record) {
- if (onSendMessage) {
- const contextMessage = Object.entries(answers)
- .map(([keyword, answer]) => `${keyword}: ${answer}`)
- .join("\n");
-
- onSendMessage(
- `I have the answers to your questions:\n\n${contextMessage}\n\nPlease proceed with creating the agent.`,
- );
- }
- }
-
- const handleCopy = useCallback(
- async function handleCopy() {
- if (message.type !== "message") return;
- if (!displayContent) return;
-
- try {
- await navigator.clipboard.writeText(displayContent);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
- } catch (error) {
- console.error("Failed to copy:", error);
- }
- },
- [displayContent, message],
- );
-
- function isLongResponse(content: string): boolean {
- return content.split("\n").length > 5;
- }
-
- const handleTryAgain = useCallback(() => {
- if (message.type !== "message" || !onSendMessage) return;
- onSendMessage(message.content, message.role === "user");
- }, [message, onSendMessage]);
-
- const handleViewExecution = useCallback(() => {
- if (message.type === "execution_started" && message.libraryAgentLink) {
- router.push(message.libraryAgentLink);
- }
- }, [message, router]);
-
- // Render credentials needed messages
- if (isCredentialsNeeded && message.type === "credentials_needed") {
- return (
-
- );
- }
-
- if (isClarificationNeeded && message.type === "clarification_needed") {
- return (
-
- );
- }
-
- // Render login needed messages
- if (isLoginNeeded && message.type === "login_needed") {
- // If user is already logged in, show success message instead of auth prompt
- if (user) {
- return (
-
-
-
-
-
-
-
-
-
- Successfully Authenticated
-
-
- You're now signed in and ready to continue
-
-
-
-
-
-
- );
- }
-
- // Show auth prompt if not logged in
- return (
-
- );
- }
-
- // Render tool call messages
- if (isToolCall && message.type === "tool_call") {
- // Check if this tool call is currently streaming
- // A tool call is streaming if:
- // 1. isStreaming is true
- // 2. This is the last tool_call message
- // 3. There's no tool_response for this tool call yet
- const isToolCallStreaming =
- isStreaming &&
- index >= 0 &&
- (() => {
- // Find the last tool_call index
- let lastToolCallIndex = -1;
- for (let i = messages.length - 1; i >= 0; i--) {
- if (messages[i].type === "tool_call") {
- lastToolCallIndex = i;
- break;
- }
- }
- // Check if this is the last tool_call and there's no response yet
- if (index === lastToolCallIndex) {
- // Check if there's a tool_response for this tool call
- const hasResponse = messages
- .slice(index + 1)
- .some(
- (msg) =>
- msg.type === "tool_response" && msg.toolId === message.toolId,
- );
- return !hasResponse;
- }
- return false;
- })();
-
- return (
-
-
-
- );
- }
-
- // Render no_results messages - use dedicated component, not ToolResponseMessage
- if (message.type === "no_results") {
- return (
-
-
-
- );
- }
-
- // Render agent_carousel messages - use dedicated component, not ToolResponseMessage
- if (message.type === "agent_carousel") {
- return (
-
- );
- }
-
- // Render execution_started messages - use dedicated component, not ToolResponseMessage
- if (message.type === "execution_started") {
- return (
-
-
-
- );
- }
-
- // Render tool response messages (but skip agent_output if it's being rendered inside assistant message)
- if (isToolResponse && message.type === "tool_response") {
- return (
-
-
-
- );
- }
-
- // Render regular chat messages
- if (message.type === "message") {
- return (
-
-
-
- {isUser ? (
-
-
-
- ) : (
-
-
- {agentOutput && agentOutput.type === "tool_response" && (
-
-
-
- )}
-
- )}
-
- {isUser && onSendMessage && (
-
- )}
- {!isUser && isFinalMessage && isLongResponse(displayContent) && (
-
- )}
-
-
-
-
- );
- }
-
- // Fallback for unknown message types
- return null;
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatMessage/useChatMessage.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatMessage/useChatMessage.ts
deleted file mode 100644
index 142b140c8b..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ChatMessage/useChatMessage.ts
+++ /dev/null
@@ -1,128 +0,0 @@
-import type { ToolArguments, ToolResult } from "@/types/chat";
-import { formatDistanceToNow } from "date-fns";
-
-export type ChatMessageData =
- | {
- type: "message";
- role: "user" | "assistant" | "system";
- content: string;
- timestamp?: string | Date;
- }
- | {
- type: "tool_call";
- toolId: string;
- toolName: string;
- arguments?: ToolArguments;
- timestamp?: string | Date;
- }
- | {
- type: "tool_response";
- toolId: string;
- toolName: string;
- result: ToolResult;
- success?: boolean;
- timestamp?: string | Date;
- }
- | {
- type: "login_needed";
- toolName: string;
- message: string;
- sessionId: string;
- agentInfo?: {
- graph_id: string;
- name: string;
- trigger_type: string;
- };
- timestamp?: string | Date;
- }
- | {
- type: "credentials_needed";
- toolName: string;
- credentials: Array<{
- provider: string;
- providerName: string;
- credentialTypes: Array<
- "api_key" | "oauth2" | "user_password" | "host_scoped"
- >;
- title: string;
- scopes?: string[];
- }>;
- message: string;
- agentName?: string;
- timestamp?: string | Date;
- }
- | {
- type: "no_results";
- toolName: string;
- message: string;
- suggestions?: string[];
- sessionId?: string;
- timestamp?: string | Date;
- }
- | {
- type: "agent_carousel";
- toolName: string;
- agents: Array<{
- id: string;
- name: string;
- description: string;
- version?: number;
- image_url?: string;
- }>;
- totalCount?: number;
- timestamp?: string | Date;
- }
- | {
- type: "execution_started";
- toolName: string;
- executionId: string;
- agentName?: string;
- message?: string;
- libraryAgentLink?: string;
- timestamp?: string | Date;
- }
- | {
- type: "inputs_needed";
- toolName: string;
- agentName?: string;
- agentId?: string;
- graphVersion?: number;
- inputSchema: Record;
- credentialsSchema?: Record;
- message: string;
- timestamp?: string | Date;
- }
- | {
- type: "clarification_needed";
- toolName: string;
- questions: Array<{
- question: string;
- keyword: string;
- example?: string;
- }>;
- message: string;
- sessionId: string;
- timestamp?: string | Date;
- };
-
-export function useChatMessage(message: ChatMessageData) {
- const formattedTimestamp = message.timestamp
- ? formatDistanceToNow(new Date(message.timestamp), { addSuffix: true })
- : "Just now";
-
- return {
- formattedTimestamp,
- isUser: message.type === "message" && message.role === "user",
- isAssistant: message.type === "message" && message.role === "assistant",
- isSystem: message.type === "message" && message.role === "system",
- isToolCall: message.type === "tool_call",
- isToolResponse: message.type === "tool_response",
- isLoginNeeded: message.type === "login_needed",
- isCredentialsNeeded: message.type === "credentials_needed",
- isNoResults: message.type === "no_results",
- isAgentCarousel: message.type === "agent_carousel",
- isExecutionStarted: message.type === "execution_started",
- isInputsNeeded: message.type === "inputs_needed",
- isClarificationNeeded: message.type === "clarification_needed",
- };
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ExecutionStartedMessage/ExecutionStartedMessage.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ExecutionStartedMessage/ExecutionStartedMessage.tsx
deleted file mode 100644
index 1ac3b440e0..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ExecutionStartedMessage/ExecutionStartedMessage.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import { Button } from "@/components/atoms/Button/Button";
-import { Text } from "@/components/atoms/Text/Text";
-import { cn } from "@/lib/utils";
-import { ArrowSquareOut, CheckCircle, Play } from "@phosphor-icons/react";
-
-export interface ExecutionStartedMessageProps {
- executionId: string;
- agentName?: string;
- message?: string;
- onViewExecution?: () => void;
- className?: string;
-}
-
-export function ExecutionStartedMessage({
- executionId,
- agentName,
- message = "Agent execution started successfully",
- onViewExecution,
- className,
-}: ExecutionStartedMessageProps) {
- return (
-
- {/* Icon & Header */}
-
-
-
-
-
-
- Execution Started
-
-
- {message}
-
-
-
-
- {/* Details */}
-
-
- {agentName && (
-
-
- Agent:
-
-
- {agentName}
-
-
- )}
-
-
- Execution ID:
-
-
- {executionId.slice(0, 16)}...
-
-
-
-
-
- {/* Action Buttons */}
- {onViewExecution && (
-
- )}
-
-
-
-
- Your agent is now running. You can monitor its progress in the monitor
- page.
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/MarkdownContent/MarkdownContent.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/MarkdownContent/MarkdownContent.tsx
deleted file mode 100644
index 51a0794090..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/MarkdownContent/MarkdownContent.tsx
+++ /dev/null
@@ -1,215 +0,0 @@
-"use client";
-
-import { cn } from "@/lib/utils";
-import React from "react";
-import ReactMarkdown from "react-markdown";
-import remarkGfm from "remark-gfm";
-
-interface MarkdownContentProps {
- content: string;
- className?: string;
-}
-
-interface CodeProps extends React.HTMLAttributes {
- children?: React.ReactNode;
- className?: string;
-}
-
-interface ListProps extends React.HTMLAttributes {
- children?: React.ReactNode;
- className?: string;
-}
-
-interface ListItemProps extends React.HTMLAttributes {
- children?: React.ReactNode;
- className?: string;
-}
-
-interface InputProps extends React.InputHTMLAttributes {
- type?: string;
-}
-
-export function MarkdownContent({ content, className }: MarkdownContentProps) {
- return (
-
-
{
- const isInline = !className?.includes("language-");
- if (isInline) {
- return (
-
- {children}
-
- );
- }
- return (
-
- {children}
-
- );
- },
- pre: ({ children, ...props }) => (
-
- {children}
-
- ),
- a: ({ children, href, ...props }) => (
-
- {children}
-
- ),
- strong: ({ children, ...props }) => (
-
- {children}
-
- ),
- em: ({ children, ...props }) => (
-
- {children}
-
- ),
- del: ({ children, ...props }) => (
-
- {children}
-
- ),
- ul: ({ children, ...props }: ListProps) => (
-
- ),
- ol: ({ children, ...props }) => (
-
- {children}
-
- ),
- li: ({ children, ...props }: ListItemProps) => (
-
- {children}
-
- ),
- input: ({ ...props }: InputProps) => {
- if (props.type === "checkbox") {
- return (
-
- );
- }
- return ;
- },
- blockquote: ({ children, ...props }) => (
-
- {children}
-
- ),
- h1: ({ children, ...props }) => (
-
- {children}
-
- ),
- h2: ({ children, ...props }) => (
-
- {children}
-
- ),
- h3: ({ children, ...props }) => (
-
- {children}
-
- ),
- h4: ({ children, ...props }) => (
-
- {children}
-
- ),
- h5: ({ children, ...props }) => (
-
- {children}
-
- ),
- h6: ({ children, ...props }) => (
-
- {children}
-
- ),
- p: ({ children, ...props }) => (
-
- {children}
-
- ),
- hr: ({ ...props }) => (
-
- ),
- table: ({ children, ...props }) => (
-
- ),
- th: ({ children, ...props }) => (
-
- {children}
- |
- ),
- td: ({ children, ...props }) => (
-
- {children}
- |
- ),
- }}
- >
- {content}
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageBubble/MessageBubble.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageBubble/MessageBubble.tsx
deleted file mode 100644
index 70d5d135a1..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageBubble/MessageBubble.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import { cn } from "@/lib/utils";
-import { ReactNode } from "react";
-
-export interface MessageBubbleProps {
- children: ReactNode;
- variant: "user" | "assistant";
- className?: string;
-}
-
-export function MessageBubble({
- children,
- variant,
- className,
-}: MessageBubbleProps) {
- const userTheme = {
- bg: "bg-purple-100",
- border: "border-purple-100",
- text: "text-slate-900",
- };
-
- const assistantTheme = {
- bg: "bg-slate-50/20",
- border: "border-slate-100",
- gradient: "from-slate-200/20 via-slate-300/10 to-transparent",
- text: "text-slate-900",
- };
-
- const theme = variant === "user" ? userTheme : assistantTheme;
-
- return (
-
- {/* Gradient flare background */}
-
-
- {children}
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/MessageList.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/MessageList.tsx
deleted file mode 100644
index 84f31f9d20..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/MessageList.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-"use client";
-
-import { cn } from "@/lib/utils";
-import type { ChatMessageData } from "../ChatMessage/useChatMessage";
-import { StreamingMessage } from "../StreamingMessage/StreamingMessage";
-import { ThinkingMessage } from "../ThinkingMessage/ThinkingMessage";
-import { LastToolResponse } from "./components/LastToolResponse/LastToolResponse";
-import { MessageItem } from "./components/MessageItem/MessageItem";
-import { findLastMessageIndex, shouldSkipAgentOutput } from "./helpers";
-import { useMessageList } from "./useMessageList";
-
-export interface MessageListProps {
- messages: ChatMessageData[];
- streamingChunks?: string[];
- isStreaming?: boolean;
- className?: string;
- onStreamComplete?: () => void;
- onSendMessage?: (content: string) => void;
-}
-
-export function MessageList({
- messages,
- streamingChunks = [],
- isStreaming = false,
- className,
- onStreamComplete,
- onSendMessage,
-}: MessageListProps) {
- const { messagesEndRef, messagesContainerRef } = useMessageList({
- messageCount: messages.length,
- isStreaming,
- });
-
- /**
- * Keeps this for debugging purposes 💆🏽
- */
- console.log(messages);
-
- return (
-
- {/* Top fade shadow */}
-
-
-
-
- {/* Render all persisted messages */}
- {(() => {
- const lastAssistantMessageIndex = findLastMessageIndex(
- messages,
- (msg) => msg.type === "message" && msg.role === "assistant",
- );
-
- const lastToolResponseIndex = findLastMessageIndex(
- messages,
- (msg) => msg.type === "tool_response",
- );
-
- return messages.map((message, index) => {
- // Skip agent_output tool_responses that should be rendered inside assistant messages
- if (shouldSkipAgentOutput(message, messages[index - 1])) {
- return null;
- }
-
- // Render last tool_response as AIChatBubble
- if (
- message.type === "tool_response" &&
- index === lastToolResponseIndex
- ) {
- return (
-
- );
- }
-
- return (
-
- );
- });
- })()}
-
- {/* Render thinking message when streaming but no chunks yet */}
- {isStreaming && streamingChunks.length === 0 &&
}
-
- {/* Render streaming message if active */}
- {isStreaming && streamingChunks.length > 0 && (
-
- )}
-
- {/* Invisible div to scroll to */}
-
-
-
-
- {/* Bottom fade shadow */}
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/components/LastToolResponse/LastToolResponse.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/components/LastToolResponse/LastToolResponse.tsx
deleted file mode 100644
index 15b10e5715..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/components/LastToolResponse/LastToolResponse.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import type { ChatMessageData } from "../../../ChatMessage/useChatMessage";
-import { ToolResponseMessage } from "../../../ToolResponseMessage/ToolResponseMessage";
-import { shouldSkipAgentOutput } from "../../helpers";
-
-export interface LastToolResponseProps {
- message: ChatMessageData;
- prevMessage: ChatMessageData | undefined;
-}
-
-export function LastToolResponse({
- message,
- prevMessage,
-}: LastToolResponseProps) {
- if (message.type !== "tool_response") return null;
-
- if (shouldSkipAgentOutput(message, prevMessage)) return null;
-
- return (
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/components/MessageItem/MessageItem.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/components/MessageItem/MessageItem.tsx
deleted file mode 100644
index d455633b66..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/components/MessageItem/MessageItem.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import { ChatMessage } from "../../../ChatMessage/ChatMessage";
-import type { ChatMessageData } from "../../../ChatMessage/useChatMessage";
-import { useMessageItem } from "./useMessageItem";
-
-export interface MessageItemProps {
- message: ChatMessageData;
- messages: ChatMessageData[];
- index: number;
- lastAssistantMessageIndex: number;
- isStreaming?: boolean;
- onSendMessage?: (content: string) => void;
-}
-
-export function MessageItem({
- message,
- messages,
- index,
- lastAssistantMessageIndex,
- isStreaming = false,
- onSendMessage,
-}: MessageItemProps) {
- const { messageToRender, agentOutput, isFinalMessage } = useMessageItem({
- message,
- messages,
- index,
- lastAssistantMessageIndex,
- });
-
- return (
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/components/MessageItem/useMessageItem.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/components/MessageItem/useMessageItem.ts
deleted file mode 100644
index 65c2e02cc8..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/components/MessageItem/useMessageItem.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import type { ChatMessageData } from "../../../ChatMessage/useChatMessage";
-import { isAgentOutputResult, isToolOutputPattern } from "../../helpers";
-
-export interface UseMessageItemArgs {
- message: ChatMessageData;
- messages: ChatMessageData[];
- index: number;
- lastAssistantMessageIndex: number;
-}
-
-export function useMessageItem({
- message,
- messages,
- index,
- lastAssistantMessageIndex,
-}: UseMessageItemArgs) {
- let agentOutput: ChatMessageData | undefined;
- let messageToRender: ChatMessageData = message;
-
- // Check if assistant message follows a tool_call and looks like a tool output
- if (message.type === "message" && message.role === "assistant") {
- const prevMessage = messages[index - 1];
-
- // Check if next message is an agent_output tool_response to include in current assistant message
- const nextMessage = messages[index + 1];
- if (
- nextMessage &&
- nextMessage.type === "tool_response" &&
- nextMessage.result
- ) {
- if (isAgentOutputResult(nextMessage.result)) {
- agentOutput = nextMessage;
- }
- }
-
- // Only convert to tool_response if it follows a tool_call AND looks like a tool output
- if (prevMessage && prevMessage.type === "tool_call") {
- if (isToolOutputPattern(message.content)) {
- // Convert this message to a tool_response format for rendering
- messageToRender = {
- type: "tool_response",
- toolId: prevMessage.toolId,
- toolName: prevMessage.toolName,
- result: message.content,
- success: true,
- timestamp: message.timestamp,
- } as ChatMessageData;
- }
- }
- }
-
- const isFinalMessage =
- messageToRender.type !== "message" ||
- messageToRender.role !== "assistant" ||
- index === lastAssistantMessageIndex;
-
- return {
- messageToRender,
- agentOutput,
- isFinalMessage,
- };
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/helpers.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/helpers.ts
deleted file mode 100644
index f6731c66c7..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/helpers.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import type { ChatMessageData } from "../ChatMessage/useChatMessage";
-
-export function parseToolResult(
- result: unknown,
-): Record | null {
- try {
- return typeof result === "string"
- ? JSON.parse(result)
- : (result as Record);
- } catch {
- return null;
- }
-}
-
-export function isAgentOutputResult(result: unknown): boolean {
- const parsed = parseToolResult(result);
- return parsed?.type === "agent_output";
-}
-
-export function isToolOutputPattern(content: string): boolean {
- const normalizedContent = content.toLowerCase().trim();
-
- return (
- normalizedContent.startsWith("no agents found") ||
- normalizedContent.startsWith("no results found") ||
- normalizedContent.includes("no agents found matching") ||
- !!normalizedContent.match(/^no \w+ found/i) ||
- (content.length < 150 && normalizedContent.includes("try different")) ||
- (content.length < 200 &&
- !normalizedContent.includes("i'll") &&
- !normalizedContent.includes("let me") &&
- !normalizedContent.includes("i can") &&
- !normalizedContent.includes("i will"))
- );
-}
-
-export function formatToolResultValue(result: unknown): string {
- return typeof result === "string"
- ? result
- : result
- ? JSON.stringify(result, null, 2)
- : "";
-}
-
-export function findLastMessageIndex(
- messages: ChatMessageData[],
- predicate: (msg: ChatMessageData) => boolean,
-): number {
- for (let i = messages.length - 1; i >= 0; i--) {
- if (predicate(messages[i])) return i;
- }
- return -1;
-}
-
-export function shouldSkipAgentOutput(
- message: ChatMessageData,
- prevMessage: ChatMessageData | undefined,
-): boolean {
- if (message.type !== "tool_response" || !message.result) return false;
-
- const isAgentOutput = isAgentOutputResult(message.result);
- return (
- isAgentOutput &&
- !!prevMessage &&
- prevMessage.type === "message" &&
- prevMessage.role === "assistant"
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/useMessageList.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/useMessageList.ts
deleted file mode 100644
index 3dcc75df3c..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/MessageList/useMessageList.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { useEffect, useRef, useCallback } from "react";
-
-interface UseMessageListArgs {
- messageCount: number;
- isStreaming: boolean;
-}
-
-export function useMessageList({
- messageCount,
- isStreaming,
-}: UseMessageListArgs) {
- const messagesEndRef = useRef(null);
- const messagesContainerRef = useRef(null);
-
- const scrollToBottom = useCallback(() => {
- messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
- }, []);
-
- useEffect(() => {
- scrollToBottom();
- }, [messageCount, isStreaming, scrollToBottom]);
-
- return {
- messagesEndRef,
- messagesContainerRef,
- scrollToBottom,
- };
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/NoResultsMessage/NoResultsMessage.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/NoResultsMessage/NoResultsMessage.tsx
deleted file mode 100644
index b6adc8b93c..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/NoResultsMessage/NoResultsMessage.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import { Text } from "@/components/atoms/Text/Text";
-import { cn } from "@/lib/utils";
-import { MagnifyingGlass, X } from "@phosphor-icons/react";
-
-export interface NoResultsMessageProps {
- message: string;
- suggestions?: string[];
- className?: string;
-}
-
-export function NoResultsMessage({
- message,
- suggestions = [],
- className,
-}: NoResultsMessageProps) {
- return (
-
- {/* Icon */}
-
-
- {/* Content */}
-
-
- No Results Found
-
-
- {message}
-
-
-
- {/* Suggestions */}
- {suggestions.length > 0 && (
-
-
- Try these suggestions:
-
-
- {suggestions.map((suggestion, index) => (
- -
- •
- {suggestion}
-
- ))}
-
-
- )}
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/QuickActionsWelcome/QuickActionsWelcome.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/QuickActionsWelcome/QuickActionsWelcome.tsx
deleted file mode 100644
index dd76fd9fb6..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/QuickActionsWelcome/QuickActionsWelcome.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-"use client";
-
-import { Text } from "@/components/atoms/Text/Text";
-import { cn } from "@/lib/utils";
-
-export interface QuickActionsWelcomeProps {
- title: string;
- description: string;
- actions: string[];
- onActionClick: (action: string) => void;
- disabled?: boolean;
- className?: string;
-}
-
-export function QuickActionsWelcome({
- title,
- description,
- actions,
- onActionClick,
- disabled = false,
- className,
-}: QuickActionsWelcomeProps) {
- return (
-
-
-
-
- {title}
-
-
- {description}
-
-
-
- {actions.map((action) => {
- // Use slate theme for all cards
- const theme = {
- bg: "bg-slate-50/10",
- border: "border-slate-100",
- hoverBg: "hover:bg-slate-50/20",
- hoverBorder: "hover:border-slate-200",
- gradient: "from-slate-200/20 via-slate-300/10 to-transparent",
- text: "text-slate-900",
- hoverText: "group-hover:text-slate-900",
- };
-
- return (
-
- );
- })}
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/SessionsDrawer/SessionsDrawer.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/SessionsDrawer/SessionsDrawer.tsx
deleted file mode 100644
index ee2896f1e2..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/SessionsDrawer/SessionsDrawer.tsx
+++ /dev/null
@@ -1,136 +0,0 @@
-"use client";
-
-import { useGetV2ListSessions } from "@/app/api/__generated__/endpoints/chat/chat";
-import { Text } from "@/components/atoms/Text/Text";
-import { scrollbarStyles } from "@/components/styles/scrollbars";
-import { cn } from "@/lib/utils";
-import { X } from "@phosphor-icons/react";
-import { formatDistanceToNow } from "date-fns";
-import { Drawer } from "vaul";
-
-interface SessionsDrawerProps {
- isOpen: boolean;
- onClose: () => void;
- onSelectSession: (sessionId: string) => void;
- currentSessionId?: string | null;
-}
-
-export function SessionsDrawer({
- isOpen,
- onClose,
- onSelectSession,
- currentSessionId,
-}: SessionsDrawerProps) {
- const { data, isLoading } = useGetV2ListSessions(
- { limit: 100 },
- {
- query: {
- enabled: isOpen,
- },
- },
- );
-
- const sessions =
- data?.status === 200
- ? data.data.sessions.filter((session) => {
- // Filter out sessions without messages (sessions that were never updated)
- // If updated_at equals created_at, the session was created but never had messages
- return session.updated_at !== session.created_at;
- })
- : [];
-
- function handleSelectSession(sessionId: string) {
- onSelectSession(sessionId);
- onClose();
- }
-
- return (
- !open && onClose()}
- direction="right"
- >
-
-
-
-
-
-
- Chat Sessions
-
-
-
-
-
-
- {isLoading ? (
-
-
- Loading sessions...
-
-
- ) : sessions.length === 0 ? (
-
-
- You don't have previously started chats
-
-
- ) : (
-
- {sessions.map((session) => {
- const isActive = session.id === currentSessionId;
- const updatedAt = session.updated_at
- ? formatDistanceToNow(new Date(session.updated_at), {
- addSuffix: true,
- })
- : "";
-
- return (
-
- );
- })}
-
- )}
-
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/StreamingMessage/StreamingMessage.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/StreamingMessage/StreamingMessage.tsx
deleted file mode 100644
index 5b1f8b1617..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/StreamingMessage/StreamingMessage.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { cn } from "@/lib/utils";
-import { AIChatBubble } from "../AIChatBubble/AIChatBubble";
-import { MarkdownContent } from "../MarkdownContent/MarkdownContent";
-import { useStreamingMessage } from "./useStreamingMessage";
-
-export interface StreamingMessageProps {
- chunks: string[];
- className?: string;
- onComplete?: () => void;
-}
-
-export function StreamingMessage({
- chunks,
- className,
- onComplete,
-}: StreamingMessageProps) {
- const { displayText } = useStreamingMessage({ chunks, onComplete });
-
- return (
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/StreamingMessage/useStreamingMessage.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/StreamingMessage/useStreamingMessage.ts
deleted file mode 100644
index 5203762151..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/StreamingMessage/useStreamingMessage.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { useEffect, useState } from "react";
-
-interface UseStreamingMessageArgs {
- chunks: string[];
- onComplete?: () => void;
-}
-
-export function useStreamingMessage({
- chunks,
- onComplete,
-}: UseStreamingMessageArgs) {
- const [isComplete, _setIsComplete] = useState(false);
- const displayText = chunks.join("");
-
- useEffect(() => {
- if (isComplete && onComplete) {
- onComplete();
- }
- }, [isComplete, onComplete]);
-
- return {
- displayText,
- isComplete,
- };
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ThinkingMessage/ThinkingMessage.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ThinkingMessage/ThinkingMessage.tsx
deleted file mode 100644
index 047c2277b0..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ThinkingMessage/ThinkingMessage.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import { cn } from "@/lib/utils";
-import { useEffect, useRef, useState } from "react";
-import { AIChatBubble } from "../AIChatBubble/AIChatBubble";
-
-export interface ThinkingMessageProps {
- className?: string;
-}
-
-export function ThinkingMessage({ className }: ThinkingMessageProps) {
- const [showSlowLoader, setShowSlowLoader] = useState(false);
- const [showCoffeeMessage, setShowCoffeeMessage] = useState(false);
- const timerRef = useRef(null);
- const coffeeTimerRef = useRef(null);
-
- useEffect(() => {
- if (timerRef.current === null) {
- timerRef.current = setTimeout(() => {
- setShowSlowLoader(true);
- }, 8000);
- }
-
- if (coffeeTimerRef.current === null) {
- coffeeTimerRef.current = setTimeout(() => {
- setShowCoffeeMessage(true);
- }, 10000);
- }
-
- return () => {
- if (timerRef.current) {
- clearTimeout(timerRef.current);
- timerRef.current = null;
- }
- if (coffeeTimerRef.current) {
- clearTimeout(coffeeTimerRef.current);
- coffeeTimerRef.current = null;
- }
- };
- }, []);
-
- return (
-
-
-
-
-
- {showCoffeeMessage ? (
-
- This could take a few minutes, grab a coffee ☕️
-
- ) : showSlowLoader ? (
-
- Taking a bit more time...
-
- ) : (
-
- Thinking...
-
- )}
-
-
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolCallMessage/ToolCallMessage.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolCallMessage/ToolCallMessage.tsx
deleted file mode 100644
index 67aa975f15..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolCallMessage/ToolCallMessage.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import { Text } from "@/components/atoms/Text/Text";
-import { cn } from "@/lib/utils";
-import type { ToolArguments } from "@/types/chat";
-import { AIChatBubble } from "../AIChatBubble/AIChatBubble";
-import {
- formatToolArguments,
- getToolActionPhrase,
- getToolIcon,
-} from "./helpers";
-
-export interface ToolCallMessageProps {
- toolId?: string;
- toolName: string;
- arguments?: ToolArguments;
- isStreaming?: boolean;
- className?: string;
-}
-
-export function ToolCallMessage({
- toolName,
- arguments: toolArguments,
- isStreaming = false,
- className,
-}: ToolCallMessageProps) {
- const actionPhrase = getToolActionPhrase(toolName);
- const argumentsText = formatToolArguments(toolName, toolArguments);
- const displayText = `${actionPhrase}${argumentsText}`;
- const IconComponent = getToolIcon(toolName);
-
- return (
-
-
-
-
- {displayText}
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolCallMessage/helpers.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolCallMessage/helpers.ts
deleted file mode 100644
index 2f9f50b455..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolCallMessage/helpers.ts
+++ /dev/null
@@ -1,184 +0,0 @@
-import type { ToolArguments } from "@/types/chat";
-import {
- BrainIcon,
- EyeIcon,
- FileMagnifyingGlassIcon,
- FileTextIcon,
- MagnifyingGlassIcon,
- PackageIcon,
- PencilLineIcon,
- PlayIcon,
- PlusIcon,
- SquaresFourIcon,
- type Icon,
-} from "@phosphor-icons/react";
-
-/**
- * Maps internal tool names to human-friendly action phrases (present continuous).
- * Used for tool call messages to indicate what action is currently happening.
- *
- * @param toolName - The internal tool name from the backend
- * @returns A human-friendly action phrase in present continuous tense
- */
-export function getToolActionPhrase(toolName: string): string {
- const normalizedName = toolName.trim();
- if (!normalizedName) return "Executing";
- if (normalizedName.toLowerCase().startsWith("executing")) {
- return normalizedName;
- }
- if (normalizedName.toLowerCase() === "unknown") return "Executing";
- const toolActionPhrases: Record = {
- add_understanding: "Updating your business information",
- create_agent: "Creating a new agent",
- edit_agent: "Editing the agent",
- find_agent: "Looking for agents in the marketplace",
- find_block: "Searching for blocks",
- find_library_agent: "Looking for library agents",
- run_agent: "Running the agent",
- run_block: "Running the block",
- view_agent_output: "Retrieving agent output",
- search_docs: "Searching documentation",
- get_doc_page: "Loading documentation page",
- agent_carousel: "Looking for agents in the marketplace",
- execution_started: "Running the agent",
- get_required_setup_info: "Getting setup requirements",
- schedule_agent: "Scheduling the agent to run",
- };
-
- // Return mapped phrase or generate human-friendly fallback
- return (
- toolActionPhrases[toolName] ||
- toolName
- .replace(/_/g, " ")
- .replace(/\b\w/g, (l) => l.toUpperCase())
- .replace(/^/, "Executing ")
- );
-}
-
-/**
- * Formats tool call arguments into user-friendly text.
- * Handles different tool types and formats their arguments nicely.
- *
- * @param toolName - The tool name
- * @param args - The tool arguments
- * @returns Formatted user-friendly text to append to action phrase
- */
-export function formatToolArguments(
- toolName: string,
- args: ToolArguments | undefined,
-): string {
- if (!args || Object.keys(args).length === 0) {
- return "";
- }
-
- switch (toolName) {
- case "find_agent":
- case "find_library_agent":
- case "agent_carousel":
- if (args.query) {
- return ` matching "${args.query as string}"`;
- }
- break;
-
- case "find_block":
- if (args.query) {
- return ` matching "${args.query as string}"`;
- }
- break;
-
- case "search_docs":
- if (args.query) {
- return ` for "${args.query as string}"`;
- }
- break;
-
- case "get_doc_page":
- if (args.path) {
- return ` "${args.path as string}"`;
- }
- break;
-
- case "run_agent":
- if (args.username_agent_slug) {
- return ` "${args.username_agent_slug as string}"`;
- }
- if (args.library_agent_id) {
- return ` (library agent)`;
- }
- break;
-
- case "run_block":
- if (args.block_id) {
- return ` "${args.block_id as string}"`;
- }
- break;
-
- case "view_agent_output":
- if (args.library_agent_id) {
- return ` (library agent)`;
- }
- if (args.username_agent_slug) {
- return ` "${args.username_agent_slug as string}"`;
- }
- break;
-
- case "create_agent":
- case "edit_agent":
- if (args.name) {
- return ` "${args.name as string}"`;
- }
- break;
-
- case "add_understanding":
- const understandingFields = Object.entries(args)
- .filter(
- ([_, value]) => value !== null && value !== undefined && value !== "",
- )
- .map(([key, value]) => {
- if (key === "user_name" && typeof value === "string") {
- return `for ${value}`;
- }
- if (typeof value === "string") {
- return `${key}: ${value}`;
- }
- if (Array.isArray(value) && value.length > 0) {
- return `${key}: ${value.slice(0, 2).join(", ")}${value.length > 2 ? ` (+${value.length - 2} more)` : ""}`;
- }
- return key;
- });
- if (understandingFields.length > 0) {
- return ` ${understandingFields[0]}`;
- }
- break;
- }
-
- return "";
-}
-
-/**
- * Maps tool names to their corresponding Phosphor icon components.
- *
- * @param toolName - The tool name from the backend
- * @returns The Icon component for the tool
- */
-export function getToolIcon(toolName: string): Icon {
- const iconMap: Record = {
- add_understanding: BrainIcon,
- create_agent: PlusIcon,
- edit_agent: PencilLineIcon,
- find_agent: SquaresFourIcon,
- find_library_agent: MagnifyingGlassIcon,
- find_block: PackageIcon,
- run_agent: PlayIcon,
- run_block: PlayIcon,
- view_agent_output: EyeIcon,
- search_docs: FileMagnifyingGlassIcon,
- get_doc_page: FileTextIcon,
- agent_carousel: MagnifyingGlassIcon,
- execution_started: PlayIcon,
- get_required_setup_info: SquaresFourIcon,
- schedule_agent: PlayIcon,
- };
-
- return iconMap[toolName] || SquaresFourIcon;
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolResponseMessage/ToolResponseMessage.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolResponseMessage/ToolResponseMessage.tsx
deleted file mode 100644
index 27da02beb8..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolResponseMessage/ToolResponseMessage.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import { Text } from "@/components/atoms/Text/Text";
-import { cn } from "@/lib/utils";
-import type { ToolResult } from "@/types/chat";
-import { WarningCircleIcon } from "@phosphor-icons/react";
-import { AIChatBubble } from "../AIChatBubble/AIChatBubble";
-import { MarkdownContent } from "../MarkdownContent/MarkdownContent";
-import {
- formatToolResponse,
- getErrorMessage,
- isErrorResponse,
-} from "./helpers";
-
-export interface ToolResponseMessageProps {
- toolId?: string;
- toolName: string;
- result?: ToolResult;
- success?: boolean;
- className?: string;
-}
-
-export function ToolResponseMessage({
- toolId: _toolId,
- toolName,
- result,
- success: _success,
- className,
-}: ToolResponseMessageProps) {
- if (isErrorResponse(result)) {
- const errorMessage = getErrorMessage(result);
- return (
-
-
-
-
- {errorMessage}
-
-
-
- );
- }
-
- const formattedText = formatToolResponse(result, toolName);
-
- return (
-
-
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolResponseMessage/helpers.ts b/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolResponseMessage/helpers.ts
deleted file mode 100644
index 400f32936e..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/ToolResponseMessage/helpers.ts
+++ /dev/null
@@ -1,295 +0,0 @@
-function stripInternalReasoning(content: string): string {
- return content
- .replace(/[\s\S]*?<\/internal_reasoning>/gi, "")
- .replace(/[\s\S]*?<\/thinking>/gi, "")
- .replace(/\n{3,}/g, "\n\n")
- .trim();
-}
-
-export function isErrorResponse(result: unknown): boolean {
- if (typeof result === "string") {
- const lower = result.toLowerCase();
- return (
- lower.startsWith("error:") ||
- lower.includes("not found") ||
- lower.includes("does not exist") ||
- lower.includes("failed to") ||
- lower.includes("unable to")
- );
- }
- if (typeof result === "object" && result !== null) {
- const response = result as Record;
- return response.type === "error" || response.error !== undefined;
- }
- return false;
-}
-
-export function getErrorMessage(result: unknown): string {
- if (typeof result === "string") {
- return stripInternalReasoning(result.replace(/^error:\s*/i, ""));
- }
- if (typeof result === "object" && result !== null) {
- const response = result as Record;
- if (response.error) return stripInternalReasoning(String(response.error));
- if (response.message)
- return stripInternalReasoning(String(response.message));
- }
- return "An error occurred";
-}
-
-function getToolCompletionPhrase(toolName: string): string {
- const toolCompletionPhrases: Record = {
- add_understanding: "Updated your business information",
- create_agent: "Created the agent",
- edit_agent: "Updated the agent",
- find_agent: "Found agents in the marketplace",
- find_block: "Found blocks",
- find_library_agent: "Found library agents",
- run_agent: "Started agent execution",
- run_block: "Executed the block",
- view_agent_output: "Retrieved agent output",
- search_docs: "Found documentation",
- get_doc_page: "Loaded documentation page",
- };
-
- // Return mapped phrase or generate human-friendly fallback
- return (
- toolCompletionPhrases[toolName] ||
- `Completed ${toolName.replace(/_/g, " ").replace("...", "")}`
- );
-}
-
-export function formatToolResponse(result: unknown, toolName: string): string {
- if (typeof result === "string") {
- const trimmed = result.trim();
- if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
- try {
- const parsed = JSON.parse(trimmed);
- return formatToolResponse(parsed, toolName);
- } catch {
- return stripInternalReasoning(trimmed);
- }
- }
- return stripInternalReasoning(result);
- }
-
- if (typeof result !== "object" || result === null) {
- return String(result);
- }
-
- const response = result as Record;
-
- // Handle different response types
- const responseType = response.type as string | undefined;
-
- if (!responseType) {
- if (response.message) {
- return String(response.message);
- }
- return getToolCompletionPhrase(toolName);
- }
-
- switch (responseType) {
- case "agents_found":
- const agents = (response.agents as Array<{ name: string }>) || [];
- const count =
- typeof response.count === "number" && !isNaN(response.count)
- ? response.count
- : agents.length;
- if (count === 0) {
- return "No agents found matching your search.";
- }
- return `Found ${count} agent${count === 1 ? "" : "s"}: ${agents
- .slice(0, 3)
- .map((a) => a.name)
- .join(", ")}${count > 3 ? ` and ${count - 3} more` : ""}`;
-
- case "agent_details":
- const agent = response.agent as { name: string; description?: string };
- if (agent) {
- return `Agent: ${agent.name}${agent.description ? `\n\n${agent.description}` : ""}`;
- }
- break;
-
- case "block_list":
- const blocks = (response.blocks as Array<{ name: string }>) || [];
- const blockCount =
- typeof response.count === "number" && !isNaN(response.count)
- ? response.count
- : blocks.length;
- if (blockCount === 0) {
- return "No blocks found matching your search.";
- }
- return `Found ${blockCount} block${blockCount === 1 ? "" : "s"}: ${blocks
- .slice(0, 3)
- .map((b) => b.name)
- .join(", ")}${blockCount > 3 ? ` and ${blockCount - 3} more` : ""}`;
-
- case "block_output":
- const blockName = (response.block_name as string) || "Block";
- const outputs = response.outputs as Record | undefined;
- if (outputs && Object.keys(outputs).length > 0) {
- const outputKeys = Object.keys(outputs);
- return `${blockName} executed successfully. Outputs: ${outputKeys.join(", ")}`;
- }
- return `${blockName} executed successfully.`;
-
- case "doc_search_results":
- const docResults = (response.results as Array<{ title: string }>) || [];
- const docCount =
- typeof response.count === "number" && !isNaN(response.count)
- ? response.count
- : docResults.length;
- if (docCount === 0) {
- return "No documentation found matching your search.";
- }
- return `Found ${docCount} documentation result${docCount === 1 ? "" : "s"}: ${docResults
- .slice(0, 3)
- .map((r) => r.title)
- .join(", ")}${docCount > 3 ? ` and ${docCount - 3} more` : ""}`;
-
- case "doc_page":
- const title = (response.title as string) || "Documentation";
- const content = (response.content as string) || "";
- if (content) {
- const preview = content.substring(0, 200).trim();
- return `${title}\n\n${preview}${content.length > 200 ? "..." : ""}`;
- }
- return title;
-
- case "understanding_updated":
- const currentUnderstanding = response.current_understanding as
- | Record
- | undefined;
- const fields = (response.updated_fields as string[]) || [];
-
- if (response.message && typeof response.message === "string") {
- let message = response.message;
- if (currentUnderstanding) {
- for (const [key, value] of Object.entries(currentUnderstanding)) {
- if (value !== null && value !== undefined && value !== "") {
- const placeholder = key;
- const actualValue = String(value);
- message = message.replace(
- new RegExp(`\\b${placeholder}\\b`, "g"),
- actualValue,
- );
- }
- }
- }
- return message;
- }
-
- if (
- currentUnderstanding &&
- Object.keys(currentUnderstanding).length > 0
- ) {
- const understandingEntries = Object.entries(currentUnderstanding)
- .filter(
- ([_, value]) =>
- value !== null && value !== undefined && value !== "",
- )
- .map(([key, value]) => {
- if (key === "user_name" && typeof value === "string") {
- return `Updated information for ${value}`;
- }
- return `${key}: ${String(value)}`;
- });
- if (understandingEntries.length > 0) {
- return understandingEntries[0];
- }
- }
- if (fields.length > 0) {
- return `Updated business information: ${fields.join(", ")}`;
- }
- return "Updated your business information.";
-
- case "agent_saved":
- const agentName = (response.agent_name as string) || "Agent";
- return `Successfully saved "${agentName}" to your library.`;
-
- case "agent_preview":
- const previewAgentName = (response.agent_name as string) || "Agent";
- const nodeCount = (response.node_count as number) || 0;
- const linkCount = (response.link_count as number) || 0;
- const description = (response.description as string) || "";
- let previewText = `Preview: "${previewAgentName}"`;
- if (description) {
- previewText += `\n\n${description}`;
- }
- previewText += `\n\n${nodeCount} node${nodeCount === 1 ? "" : "s"}, ${linkCount} link${linkCount === 1 ? "" : "s"}`;
- return previewText;
-
- case "clarification_needed":
- const questions =
- (response.questions as Array<{ question: string }>) || [];
- if (questions.length === 0) {
- return response.message
- ? String(response.message)
- : "I need more information to proceed.";
- }
- if (questions.length === 1) {
- return questions[0].question;
- }
- return `I need clarification on ${questions.length} points:\n\n${questions
- .map((q, i) => `${i + 1}. ${q.question}`)
- .join("\n")}`;
-
- case "agent_output":
- if (response.message) {
- return String(response.message);
- }
- const outputAgentName = (response.agent_name as string) || "Agent";
- const execution = response.execution as
- | {
- status?: string;
- outputs?: Record;
- }
- | undefined;
- if (execution) {
- const status = execution.status || "completed";
- const outputs = execution.outputs || {};
- const outputKeys = Object.keys(outputs);
- if (outputKeys.length > 0) {
- return `${outputAgentName} execution ${status}. Outputs: ${outputKeys.join(", ")}`;
- }
- return `${outputAgentName} execution ${status}.`;
- }
- return `${outputAgentName} output retrieved.`;
-
- case "execution_started":
- const execAgentName = (response.graph_name as string) || "Agent";
- if (response.message) {
- return String(response.message);
- }
- return `Started execution of "${execAgentName}".`;
-
- case "error":
- const errorMsg =
- (response.error as string) || response.message || "An error occurred";
- return `Error: ${errorMsg}`;
-
- case "no_results":
- const suggestions = (response.suggestions as string[]) || [];
- let noResultsText = (response.message as string) || "No results found.";
- if (suggestions.length > 0) {
- noResultsText += `\n\nSuggestions: ${suggestions.join(", ")}`;
- }
- return noResultsText;
-
- default:
- // Try to extract a message field
- if (response.message) {
- return String(response.message);
- }
- // Fallback: try to stringify nicely
- if (Object.keys(response).length === 0) {
- return getToolCompletionPhrase(toolName);
- }
- // If we have a response object but no recognized type, try to format it nicely
- // Don't return raw JSON - return a completion phrase instead
- return getToolCompletionPhrase(toolName);
- }
-
- return getToolCompletionPhrase(toolName);
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/components/UserChatBubble/UserChatBubble.tsx b/autogpt_platform/frontend/src/components/contextual/Chat/components/UserChatBubble/UserChatBubble.tsx
deleted file mode 100644
index 46459ff894..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/components/UserChatBubble/UserChatBubble.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { cn } from "@/lib/utils";
-import { ReactNode } from "react";
-
-export interface UserChatBubbleProps {
- children: ReactNode;
- className?: string;
-}
-
-export function UserChatBubble({ children, className }: UserChatBubbleProps) {
- return (
-
- );
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/stream-executor.ts b/autogpt_platform/frontend/src/components/contextual/Chat/stream-executor.ts
deleted file mode 100644
index b0d970c286..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/stream-executor.ts
+++ /dev/null
@@ -1,142 +0,0 @@
-import type {
- ActiveStream,
- StreamChunk,
- VercelStreamChunk,
-} from "./chat-types";
-import {
- INITIAL_RETRY_DELAY,
- MAX_RETRIES,
- normalizeStreamChunk,
- parseSSELine,
-} from "./stream-utils";
-
-function notifySubscribers(stream: ActiveStream, chunk: StreamChunk) {
- stream.chunks.push(chunk);
- for (const callback of stream.onChunkCallbacks) {
- try {
- callback(chunk);
- } catch (err) {
- console.warn("[StreamExecutor] Subscriber callback error:", err);
- }
- }
-}
-
-export async function executeStream(
- stream: ActiveStream,
- message: string,
- isUserMessage: boolean,
- context?: { url: string; content: string },
- retryCount: number = 0,
-): Promise {
- const { sessionId, abortController } = stream;
-
- try {
- const url = `/api/chat/sessions/${sessionId}/stream`;
- const body = JSON.stringify({
- message,
- is_user_message: isUserMessage,
- context: context || null,
- });
-
- const response = await fetch(url, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Accept: "text/event-stream",
- },
- body,
- signal: abortController.signal,
- });
-
- if (!response.ok) {
- const errorText = await response.text();
- throw new Error(errorText || `HTTP ${response.status}`);
- }
-
- if (!response.body) {
- throw new Error("Response body is null");
- }
-
- const reader = response.body.getReader();
- const decoder = new TextDecoder();
- let buffer = "";
-
- while (true) {
- const { done, value } = await reader.read();
-
- if (done) {
- notifySubscribers(stream, { type: "stream_end" });
- stream.status = "completed";
- return;
- }
-
- buffer += decoder.decode(value, { stream: true });
- const lines = buffer.split("\n");
- buffer = lines.pop() || "";
-
- for (const line of lines) {
- const data = parseSSELine(line);
- if (data !== null) {
- if (data === "[DONE]") {
- notifySubscribers(stream, { type: "stream_end" });
- stream.status = "completed";
- return;
- }
-
- try {
- const rawChunk = JSON.parse(data) as
- | StreamChunk
- | VercelStreamChunk;
- const chunk = normalizeStreamChunk(rawChunk);
- if (!chunk) continue;
-
- notifySubscribers(stream, chunk);
-
- if (chunk.type === "stream_end") {
- stream.status = "completed";
- return;
- }
-
- if (chunk.type === "error") {
- stream.status = "error";
- stream.error = new Error(
- chunk.message || chunk.content || "Stream error",
- );
- return;
- }
- } catch (err) {
- console.warn("[StreamExecutor] Failed to parse SSE chunk:", err);
- }
- }
- }
- }
- } catch (err) {
- if (err instanceof Error && err.name === "AbortError") {
- notifySubscribers(stream, { type: "stream_end" });
- stream.status = "completed";
- return;
- }
-
- if (retryCount < MAX_RETRIES) {
- const retryDelay = INITIAL_RETRY_DELAY * Math.pow(2, retryCount);
- console.log(
- `[StreamExecutor] Retrying in ${retryDelay}ms (attempt ${retryCount + 1}/${MAX_RETRIES})`,
- );
- await new Promise((resolve) => setTimeout(resolve, retryDelay));
- return executeStream(
- stream,
- message,
- isUserMessage,
- context,
- retryCount + 1,
- );
- }
-
- stream.status = "error";
- stream.error = err instanceof Error ? err : new Error("Stream failed");
- notifySubscribers(stream, {
- type: "error",
- message: stream.error.message,
- });
- }
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/stream-utils.ts b/autogpt_platform/frontend/src/components/contextual/Chat/stream-utils.ts
deleted file mode 100644
index 4100926e79..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/stream-utils.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import type { ToolArguments, ToolResult } from "@/types/chat";
-import type { StreamChunk, VercelStreamChunk } from "./chat-types";
-
-const LEGACY_STREAM_TYPES = new Set([
- "text_chunk",
- "text_ended",
- "tool_call",
- "tool_call_start",
- "tool_response",
- "login_needed",
- "need_login",
- "credentials_needed",
- "error",
- "usage",
- "stream_end",
-]);
-
-export function isLegacyStreamChunk(
- chunk: StreamChunk | VercelStreamChunk,
-): chunk is StreamChunk {
- return LEGACY_STREAM_TYPES.has(chunk.type as StreamChunk["type"]);
-}
-
-export function normalizeStreamChunk(
- chunk: StreamChunk | VercelStreamChunk,
-): StreamChunk | null {
- if (isLegacyStreamChunk(chunk)) return chunk;
-
- switch (chunk.type) {
- case "text-delta":
- return { type: "text_chunk", content: chunk.delta };
- case "text-end":
- return { type: "text_ended" };
- case "tool-input-available":
- return {
- type: "tool_call_start",
- tool_id: chunk.toolCallId,
- tool_name: chunk.toolName,
- arguments: chunk.input as ToolArguments,
- };
- case "tool-output-available":
- return {
- type: "tool_response",
- tool_id: chunk.toolCallId,
- tool_name: chunk.toolName,
- result: chunk.output as ToolResult,
- success: chunk.success ?? true,
- };
- case "usage":
- return {
- type: "usage",
- promptTokens: chunk.promptTokens,
- completionTokens: chunk.completionTokens,
- totalTokens: chunk.totalTokens,
- };
- case "error":
- return {
- type: "error",
- message: chunk.errorText,
- code: chunk.code,
- details: chunk.details,
- };
- case "finish":
- return { type: "stream_end" };
- case "start":
- case "text-start":
- return null;
- case "tool-input-start":
- return {
- type: "tool_call_start",
- tool_id: chunk.toolCallId,
- tool_name: chunk.toolName,
- arguments: {},
- };
- }
-}
-
-export const MAX_RETRIES = 3;
-export const INITIAL_RETRY_DELAY = 1000;
-
-export function parseSSELine(line: string): string | null {
- if (line.startsWith("data: ")) return line.slice(6);
- return null;
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/useChat.ts b/autogpt_platform/frontend/src/components/contextual/Chat/useChat.ts
deleted file mode 100644
index f6b2031059..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/useChat.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-"use client";
-
-import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
-import { useEffect, useRef, useState } from "react";
-import { useChatSession } from "./useChatSession";
-import { useChatStream } from "./useChatStream";
-
-interface UseChatArgs {
- urlSessionId?: string | null;
-}
-
-export function useChat({ urlSessionId }: UseChatArgs = {}) {
- const hasClaimedSessionRef = useRef(false);
- const { user } = useSupabase();
- const { sendMessage: sendStreamMessage } = useChatStream();
- const [showLoader, setShowLoader] = useState(false);
- const {
- session,
- sessionId: sessionIdFromHook,
- messages,
- isLoading,
- isCreating,
- error,
- isSessionNotFound,
- createSession,
- claimSession,
- clearSession: clearSessionBase,
- loadSession,
- } = useChatSession({
- urlSessionId,
- autoCreate: false,
- });
-
- useEffect(
- function autoClaimSession() {
- if (
- session &&
- !session.user_id &&
- user &&
- !hasClaimedSessionRef.current &&
- !isLoading &&
- sessionIdFromHook
- ) {
- hasClaimedSessionRef.current = true;
- claimSession(sessionIdFromHook)
- .then(() => {
- sendStreamMessage(
- sessionIdFromHook,
- "User has successfully logged in.",
- () => {},
- false,
- ).catch(() => {});
- })
- .catch(() => {
- hasClaimedSessionRef.current = false;
- });
- }
- },
- [
- session,
- user,
- isLoading,
- sessionIdFromHook,
- claimSession,
- sendStreamMessage,
- ],
- );
-
- useEffect(
- function showLoaderWithDelay() {
- if (isLoading || isCreating) {
- const timer = setTimeout(() => setShowLoader(true), 300);
- return () => clearTimeout(timer);
- }
- setShowLoader(false);
- },
- [isLoading, isCreating],
- );
-
- function clearSession() {
- clearSessionBase();
- hasClaimedSessionRef.current = false;
- }
-
- return {
- session,
- messages,
- isLoading,
- isCreating,
- error,
- isSessionNotFound,
- createSession,
- clearSession,
- loadSession,
- sessionId: sessionIdFromHook,
- showLoader,
- };
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/useChatSession.ts b/autogpt_platform/frontend/src/components/contextual/Chat/useChatSession.ts
deleted file mode 100644
index dd743874f7..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/useChatSession.ts
+++ /dev/null
@@ -1,274 +0,0 @@
-import {
- getGetV2GetSessionQueryKey,
- getGetV2GetSessionQueryOptions,
- getGetV2ListSessionsQueryKey,
- postV2CreateSession,
- useGetV2GetSession,
- usePatchV2SessionAssignUser,
- usePostV2CreateSession,
-} from "@/app/api/__generated__/endpoints/chat/chat";
-import type { SessionDetailResponse } from "@/app/api/__generated__/models/sessionDetailResponse";
-import { okData } from "@/app/api/helpers";
-import { isValidUUID } from "@/lib/utils";
-import { useQueryClient } from "@tanstack/react-query";
-import { useEffect, useMemo, useRef, useState } from "react";
-import { toast } from "sonner";
-
-interface UseChatSessionArgs {
- urlSessionId?: string | null;
- autoCreate?: boolean;
-}
-
-export function useChatSession({
- urlSessionId,
- autoCreate = false,
-}: UseChatSessionArgs = {}) {
- const queryClient = useQueryClient();
- const [sessionId, setSessionId] = useState(null);
- const [error, setError] = useState(null);
- const justCreatedSessionIdRef = useRef(null);
-
- useEffect(() => {
- if (urlSessionId) {
- if (!isValidUUID(urlSessionId)) {
- console.error("Invalid session ID format:", urlSessionId);
- toast.error("Invalid session ID", {
- description:
- "The session ID in the URL is not valid. Starting a new session...",
- });
- setSessionId(null);
- return;
- }
- setSessionId(urlSessionId);
- } else if (autoCreate) {
- setSessionId(null);
- } else {
- setSessionId(null);
- }
- }, [urlSessionId, autoCreate]);
-
- const { isPending: isCreating, error: createError } =
- usePostV2CreateSession();
-
- const {
- data: sessionData,
- isLoading: isLoadingSession,
- error: loadError,
- refetch,
- } = useGetV2GetSession(sessionId || "", {
- query: {
- enabled: !!sessionId,
- select: okData,
- retry: shouldRetrySessionLoad,
- retryDelay: getSessionRetryDelay,
- },
- });
-
- const { mutateAsync: claimSessionMutation } = usePatchV2SessionAssignUser();
-
- const session = useMemo(() => {
- if (sessionData) return sessionData;
-
- if (sessionId && justCreatedSessionIdRef.current === sessionId) {
- return {
- id: sessionId,
- user_id: null,
- messages: [],
- created_at: new Date().toISOString(),
- updated_at: new Date().toISOString(),
- } as SessionDetailResponse;
- }
- return null;
- }, [sessionData, sessionId]);
-
- const messages = session?.messages || [];
- const isLoading = isCreating || isLoadingSession;
-
- useEffect(() => {
- if (createError) {
- setError(
- createError instanceof Error
- ? createError
- : new Error("Failed to create session"),
- );
- } else if (loadError) {
- setError(
- loadError instanceof Error
- ? loadError
- : new Error("Failed to load session"),
- );
- } else {
- setError(null);
- }
- }, [createError, loadError]);
-
- useEffect(
- function refreshSessionsListOnLoad() {
- if (sessionId && sessionData && !isLoadingSession) {
- queryClient.invalidateQueries({
- queryKey: getGetV2ListSessionsQueryKey(),
- });
- }
- },
- [sessionId, sessionData, isLoadingSession, queryClient],
- );
-
- async function createSession() {
- try {
- setError(null);
- const response = await postV2CreateSession({
- body: JSON.stringify({}),
- });
- if (response.status !== 200) {
- throw new Error("Failed to create session");
- }
- const newSessionId = response.data.id;
- setSessionId(newSessionId);
- justCreatedSessionIdRef.current = newSessionId;
- setTimeout(() => {
- if (justCreatedSessionIdRef.current === newSessionId) {
- justCreatedSessionIdRef.current = null;
- }
- }, 10000);
- return newSessionId;
- } catch (err) {
- const error =
- err instanceof Error ? err : new Error("Failed to create session");
- setError(error);
- toast.error("Failed to create chat session", {
- description: error.message,
- });
- throw error;
- }
- }
-
- async function loadSession(id: string) {
- try {
- setError(null);
- // Invalidate the query cache for this session to force a fresh fetch
- await queryClient.invalidateQueries({
- queryKey: getGetV2GetSessionQueryKey(id),
- });
- // Set sessionId after invalidation to ensure the hook refetches
- setSessionId(id);
- // Force fetch with fresh data (bypass cache)
- const queryOptions = getGetV2GetSessionQueryOptions(id, {
- query: {
- staleTime: 0, // Force fresh fetch
- retry: shouldRetrySessionLoad,
- retryDelay: getSessionRetryDelay,
- },
- });
- const result = await queryClient.fetchQuery(queryOptions);
- if (!result || ("status" in result && result.status !== 200)) {
- console.warn("Session not found on server");
- setSessionId(null);
- throw new Error("Session not found");
- }
- } catch (err) {
- const error =
- err instanceof Error ? err : new Error("Failed to load session");
- setError(error);
- throw error;
- }
- }
-
- async function refreshSession() {
- if (!sessionId) return;
- try {
- setError(null);
- await refetch();
- } catch (err) {
- const error =
- err instanceof Error ? err : new Error("Failed to refresh session");
- setError(error);
- throw error;
- }
- }
-
- async function claimSession(id: string) {
- try {
- setError(null);
- await claimSessionMutation({ sessionId: id });
- if (justCreatedSessionIdRef.current === id) {
- justCreatedSessionIdRef.current = null;
- }
- await queryClient.invalidateQueries({
- queryKey: getGetV2GetSessionQueryKey(id),
- });
- await refetch();
- toast.success("Session claimed successfully", {
- description: "Your chat history has been saved to your account",
- });
- } catch (err: unknown) {
- const error =
- err instanceof Error ? err : new Error("Failed to claim session");
- const is404 =
- (typeof err === "object" &&
- err !== null &&
- "status" in err &&
- err.status === 404) ||
- (typeof err === "object" &&
- err !== null &&
- "response" in err &&
- typeof err.response === "object" &&
- err.response !== null &&
- "status" in err.response &&
- err.response.status === 404);
- if (!is404) {
- setError(error);
- toast.error("Failed to claim session", {
- description: error.message || "Unable to claim session",
- });
- }
- throw error;
- }
- }
-
- function clearSession() {
- setSessionId(null);
- setError(null);
- justCreatedSessionIdRef.current = null;
- }
-
- return {
- session,
- sessionId,
- messages,
- isLoading,
- isCreating,
- error,
- isSessionNotFound: isNotFoundError(loadError),
- createSession,
- loadSession,
- refreshSession,
- claimSession,
- clearSession,
- };
-}
-
-function isNotFoundError(error: unknown): boolean {
- if (!error || typeof error !== "object") return false;
- if ("status" in error && error.status === 404) return true;
- if (
- "response" in error &&
- typeof error.response === "object" &&
- error.response !== null &&
- "status" in error.response &&
- error.response.status === 404
- ) {
- return true;
- }
- return false;
-}
-
-function shouldRetrySessionLoad(failureCount: number, error: unknown): boolean {
- if (!isNotFoundError(error)) return false;
- return failureCount <= 2;
-}
-
-function getSessionRetryDelay(attemptIndex: number): number {
- if (attemptIndex === 0) return 3000;
- if (attemptIndex === 1) return 5000;
- return 0;
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/useChatStream.ts b/autogpt_platform/frontend/src/components/contextual/Chat/useChatStream.ts
deleted file mode 100644
index 5a9f637457..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/useChatStream.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-"use client";
-
-import { useEffect, useRef, useState } from "react";
-import { toast } from "sonner";
-import { useChatStore } from "./chat-store";
-import type { StreamChunk } from "./chat-types";
-
-export type { StreamChunk } from "./chat-types";
-
-export function useChatStream() {
- const [isStreaming, setIsStreaming] = useState(false);
- const [error, setError] = useState(null);
- const currentSessionIdRef = useRef(null);
- const onChunkCallbackRef = useRef<((chunk: StreamChunk) => void) | null>(
- null,
- );
-
- const stopStream = useChatStore((s) => s.stopStream);
- const unregisterActiveSession = useChatStore(
- (s) => s.unregisterActiveSession,
- );
- const isSessionActive = useChatStore((s) => s.isSessionActive);
- const onStreamComplete = useChatStore((s) => s.onStreamComplete);
- const getCompletedStream = useChatStore((s) => s.getCompletedStream);
- const registerActiveSession = useChatStore((s) => s.registerActiveSession);
- const startStream = useChatStore((s) => s.startStream);
- const getStreamStatus = useChatStore((s) => s.getStreamStatus);
-
- function stopStreaming(sessionId?: string) {
- const targetSession = sessionId || currentSessionIdRef.current;
- if (targetSession) {
- stopStream(targetSession);
- unregisterActiveSession(targetSession);
- }
- setIsStreaming(false);
- }
-
- useEffect(() => {
- return function cleanup() {
- const sessionId = currentSessionIdRef.current;
- if (sessionId && !isSessionActive(sessionId)) {
- stopStream(sessionId);
- }
- currentSessionIdRef.current = null;
- onChunkCallbackRef.current = null;
- };
- }, []);
-
- useEffect(() => {
- const unsubscribe = onStreamComplete(
- function handleStreamComplete(completedSessionId) {
- if (completedSessionId !== currentSessionIdRef.current) return;
-
- setIsStreaming(false);
- const completed = getCompletedStream(completedSessionId);
- if (completed?.error) {
- setError(completed.error);
- }
- unregisterActiveSession(completedSessionId);
- },
- );
-
- return unsubscribe;
- }, []);
-
- async function sendMessage(
- sessionId: string,
- message: string,
- onChunk: (chunk: StreamChunk) => void,
- isUserMessage: boolean = true,
- context?: { url: string; content: string },
- ) {
- const previousSessionId = currentSessionIdRef.current;
- if (previousSessionId && previousSessionId !== sessionId) {
- stopStreaming(previousSessionId);
- }
-
- currentSessionIdRef.current = sessionId;
- onChunkCallbackRef.current = onChunk;
- setIsStreaming(true);
- setError(null);
-
- registerActiveSession(sessionId);
-
- try {
- await startStream(sessionId, message, isUserMessage, context, onChunk);
-
- const status = getStreamStatus(sessionId);
- if (status === "error") {
- const completed = getCompletedStream(sessionId);
- if (completed?.error) {
- setError(completed.error);
- toast.error("Connection Failed", {
- description: "Unable to connect to chat service. Please try again.",
- });
- throw completed.error;
- }
- }
- } catch (err) {
- const streamError =
- err instanceof Error ? err : new Error("Failed to start stream");
- setError(streamError);
- throw streamError;
- } finally {
- setIsStreaming(false);
- }
- }
-
- return {
- isStreaming,
- error,
- sendMessage,
- stopStreaming,
- };
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/Chat/usePageContext.ts b/autogpt_platform/frontend/src/components/contextual/Chat/usePageContext.ts
deleted file mode 100644
index c567422a5c..0000000000
--- a/autogpt_platform/frontend/src/components/contextual/Chat/usePageContext.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import { useCallback } from "react";
-
-export interface PageContext {
- url: string;
- content: string;
-}
-
-const MAX_CONTENT_CHARS = 10000;
-
-/**
- * Hook to capture the current page context (URL + full page content)
- * Privacy-hardened: removes sensitive inputs and enforces content size limits
- */
-export function usePageContext() {
- const capturePageContext = useCallback((): PageContext => {
- if (typeof window === "undefined" || typeof document === "undefined") {
- return { url: "", content: "" };
- }
-
- const url = window.location.href;
-
- // Clone document to avoid modifying the original
- const clone = document.cloneNode(true) as Document;
-
- // Remove script, style, and noscript elements
- const scripts = clone.querySelectorAll("script, style, noscript");
- scripts.forEach((el) => el.remove());
-
- // Remove sensitive elements and their content
- const sensitiveSelectors = [
- "input",
- "textarea",
- "[contenteditable]",
- 'input[type="password"]',
- 'input[type="email"]',
- 'input[type="tel"]',
- 'input[type="search"]',
- 'input[type="hidden"]',
- "form",
- "[data-sensitive]",
- "[data-sensitive='true']",
- ];
-
- sensitiveSelectors.forEach((selector) => {
- const elements = clone.querySelectorAll(selector);
- elements.forEach((el) => {
- // For form elements, remove the entire element
- if (el.tagName === "FORM") {
- el.remove();
- } else {
- // For inputs and textareas, clear their values but keep the element structure
- if (
- el instanceof HTMLInputElement ||
- el instanceof HTMLTextAreaElement
- ) {
- el.value = "";
- el.textContent = "";
- } else {
- // For other sensitive elements, remove them entirely
- el.remove();
- }
- }
- });
- });
-
- // Strip any remaining input values that might have been missed
- const allInputs = clone.querySelectorAll("input, textarea");
- allInputs.forEach((el) => {
- if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
- el.value = "";
- el.textContent = "";
- }
- });
-
- // Get text content from body
- const body = clone.body;
- const content = body?.textContent || body?.innerText || "";
-
- // Clean up whitespace
- let cleanedContent = content
- .replace(/\s+/g, " ")
- .replace(/\n\s*\n/g, "\n")
- .trim();
-
- // Enforce maximum content size
- if (cleanedContent.length > MAX_CONTENT_CHARS) {
- cleanedContent =
- cleanedContent.substring(0, MAX_CONTENT_CHARS) + "... [truncated]";
- }
-
- return {
- url,
- content: cleanedContent,
- };
- }, []);
-
- return { capturePageContext };
-}
diff --git a/autogpt_platform/frontend/src/components/contextual/CredentialsInput/components/HotScopedCredentialsModal/HotScopedCredentialsModal.tsx b/autogpt_platform/frontend/src/components/contextual/CredentialsInput/components/HotScopedCredentialsModal/HotScopedCredentialsModal.tsx
index 547952841b..63d2ae1ac5 100644
--- a/autogpt_platform/frontend/src/components/contextual/CredentialsInput/components/HotScopedCredentialsModal/HotScopedCredentialsModal.tsx
+++ b/autogpt_platform/frontend/src/components/contextual/CredentialsInput/components/HotScopedCredentialsModal/HotScopedCredentialsModal.tsx
@@ -41,7 +41,17 @@ export function HostScopedCredentialsModal({
const currentHost = currentUrl ? getHostFromUrl(currentUrl) : "";
const formSchema = z.object({
- host: z.string().min(1, "Host is required"),
+ host: z
+ .string()
+ .min(1, "Host is required")
+ .refine((val) => !/^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(val), {
+ message: "Enter only the host (e.g. api.example.com), not a full URL",
+ })
+ .refine((val) => !val.includes("/"), {
+ message:
+ "Enter only the host (e.g. api.example.com), without a trailing path. " +
+ "You may specify a port (e.g. api.example.com:8080) if needed.",
+ }),
title: z.string().optional(),
headers: z.record(z.string()).optional(),
});
diff --git a/autogpt_platform/frontend/src/components/contextual/CredentialsInput/helpers.ts b/autogpt_platform/frontend/src/components/contextual/CredentialsInput/helpers.ts
index ef965d5382..57653f3804 100644
--- a/autogpt_platform/frontend/src/components/contextual/CredentialsInput/helpers.ts
+++ b/autogpt_platform/frontend/src/components/contextual/CredentialsInput/helpers.ts
@@ -26,6 +26,7 @@ export const providerIcons: Partial<
nvidia: fallbackIcon,
discord: FaDiscord,
d_id: fallbackIcon,
+ elevenlabs: fallbackIcon,
google_maps: FaGoogle,
jina: fallbackIcon,
ideogram: fallbackIcon,
diff --git a/autogpt_platform/frontend/src/components/layout/Navbar/Navbar.tsx b/autogpt_platform/frontend/src/components/layout/Navbar/Navbar.tsx
index f4bdecd1df..ce5992b4be 100644
--- a/autogpt_platform/frontend/src/components/layout/Navbar/Navbar.tsx
+++ b/autogpt_platform/frontend/src/components/layout/Navbar/Navbar.tsx
@@ -47,7 +47,7 @@ export function Navbar() {
const actualLoggedInLinks = [
{ name: "Home", href: homeHref },
- ...(isChatEnabled === true ? [{ name: "Tasks", href: "/library" }] : []),
+ ...(isChatEnabled === true ? [{ name: "Agents", href: "/library" }] : []),
...loggedInLinks,
];
@@ -62,7 +62,7 @@ export function Navbar() {
) : null}