mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-03 11:24:57 -05:00
Integrate CopilotChatActionsProvider into ChatContainer and enhance RunAgent and RunBlock tools with ChatCredentialsSetup for improved credential management and user interaction.
This commit is contained in:
@@ -6,6 +6,7 @@ import { ChatInput } from "@/components/contextual/Chat/components/ChatInput/Cha
|
||||
import { postV2CreateSession } from "@/app/api/__generated__/endpoints/chat/chat";
|
||||
import { useState } from "react";
|
||||
import { parseAsString, useQueryState } from "nuqs";
|
||||
import { CopilotChatActionsProvider } from "../CopilotChatActionsProvider/CopilotChatActionsProvider";
|
||||
|
||||
export interface ChatContainerProps {
|
||||
messages: UIMessage<unknown, UIDataTypes, UITools>[];
|
||||
@@ -45,34 +46,36 @@ export const ChatContainer = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto h-full w-full max-w-3xl pb-6">
|
||||
<div className="flex h-full flex-col">
|
||||
{sessionId ? (
|
||||
<ChatMessagesContainer
|
||||
messages={messages}
|
||||
status={status}
|
||||
error={error}
|
||||
handleSubmit={handleMessageSubmit}
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
/>
|
||||
) : (
|
||||
<EmptySession
|
||||
isCreating={isCreating}
|
||||
onCreateSession={createSession}
|
||||
/>
|
||||
)}
|
||||
<div className="relative px-3 pt-2">
|
||||
<div className="pointer-events-none absolute top-[-18px] z-10 h-6 w-full bg-gradient-to-b from-transparent to-[#f8f8f9]" />
|
||||
<ChatInput
|
||||
onSend={onSend}
|
||||
disabled={status === "streaming" || !sessionId}
|
||||
isStreaming={status === "streaming"}
|
||||
onStop={() => {}}
|
||||
placeholder="You can search or just ask"
|
||||
/>
|
||||
<CopilotChatActionsProvider onSend={onSend}>
|
||||
<div className="mx-auto h-full w-full max-w-3xl pb-6">
|
||||
<div className="flex h-full flex-col">
|
||||
{sessionId ? (
|
||||
<ChatMessagesContainer
|
||||
messages={messages}
|
||||
status={status}
|
||||
error={error}
|
||||
handleSubmit={handleMessageSubmit}
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
/>
|
||||
) : (
|
||||
<EmptySession
|
||||
isCreating={isCreating}
|
||||
onCreateSession={createSession}
|
||||
/>
|
||||
)}
|
||||
<div className="relative px-3 pt-2">
|
||||
<div className="pointer-events-none absolute top-[-18px] z-10 h-6 w-full bg-gradient-to-b from-transparent to-[#f8f8f9]" />
|
||||
<ChatInput
|
||||
onSend={onSend}
|
||||
disabled={status === "streaming" || !sessionId}
|
||||
isStreaming={status === "streaming"}
|
||||
onStop={() => {}}
|
||||
placeholder="You can search or just ask"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CopilotChatActionsProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,6 +17,8 @@ import { SearchDocsTool } from "../../tools/SearchDocs/SearchDocs";
|
||||
import { RunBlockTool } from "../../tools/RunBlock/RunBlock";
|
||||
import { RunAgentTool } from "../../tools/RunAgent/RunAgent";
|
||||
import { ViewAgentOutputTool } from "../../tools/ViewAgentOutput/ViewAgentOutput";
|
||||
import { CreateAgentTool } from "../../tools/CreateAgent/CreateAgent";
|
||||
import { EditAgentTool } from "../../tools/EditAgent/EditAgent";
|
||||
|
||||
interface ChatMessagesContainerProps {
|
||||
messages: UIMessage<unknown, UIDataTypes, UITools>[];
|
||||
@@ -90,12 +92,27 @@ export const ChatMessagesContainer = ({
|
||||
/>
|
||||
);
|
||||
case "tool-run_agent":
|
||||
case "tool-schedule_agent":
|
||||
return (
|
||||
<RunAgentTool
|
||||
key={`${message.id}-${i}`}
|
||||
part={part as ToolUIPart}
|
||||
/>
|
||||
);
|
||||
case "tool-create_agent":
|
||||
return (
|
||||
<CreateAgentTool
|
||||
key={`${message.id}-${i}`}
|
||||
part={part as ToolUIPart}
|
||||
/>
|
||||
);
|
||||
case "tool-edit_agent":
|
||||
return (
|
||||
<EditAgentTool
|
||||
key={`${message.id}-${i}`}
|
||||
part={part as ToolUIPart}
|
||||
/>
|
||||
);
|
||||
case "tool-view_agent_output":
|
||||
return (
|
||||
<ViewAgentOutputTool
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { CopilotChatActionsContext } from "./useCopilotChatActions";
|
||||
|
||||
interface Props {
|
||||
onSend: (message: string) => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function CopilotChatActionsProvider({ onSend, children }: Props) {
|
||||
return (
|
||||
<CopilotChatActionsContext.Provider value={{ onSend }}>
|
||||
{children}
|
||||
</CopilotChatActionsContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
interface CopilotChatActions {
|
||||
onSend: (message: string) => void;
|
||||
}
|
||||
|
||||
const CopilotChatActionsContext = createContext<CopilotChatActions | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
export function useCopilotChatActions(): CopilotChatActions {
|
||||
const ctx = useContext(CopilotChatActionsContext);
|
||||
if (!ctx) {
|
||||
throw new Error(
|
||||
"useCopilotChatActions must be used within CopilotChatActionsProvider",
|
||||
);
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export { CopilotChatActionsContext };
|
||||
@@ -0,0 +1,189 @@
|
||||
"use client";
|
||||
|
||||
import type { ToolUIPart } from "ai";
|
||||
import Link from "next/link";
|
||||
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
|
||||
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
|
||||
import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions";
|
||||
import { ClarificationQuestionsWidget } from "@/components/contextual/Chat/components/ClarificationQuestionsWidget/ClarificationQuestionsWidget";
|
||||
import {
|
||||
formatMaybeJson,
|
||||
getAnimationText,
|
||||
getCreateAgentToolOutput,
|
||||
StateIcon,
|
||||
truncateText,
|
||||
type CreateAgentToolOutput,
|
||||
} from "./helpers";
|
||||
|
||||
export interface CreateAgentToolPart {
|
||||
type: string;
|
||||
toolCallId: string;
|
||||
state: ToolUIPart["state"];
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
part: CreateAgentToolPart;
|
||||
}
|
||||
|
||||
function getAccordionMeta(output: CreateAgentToolOutput): {
|
||||
badgeText: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
} {
|
||||
if (output.type === "agent_saved") {
|
||||
return { badgeText: "Create agent", title: output.agent_name };
|
||||
}
|
||||
if (output.type === "agent_preview") {
|
||||
return {
|
||||
badgeText: "Create agent",
|
||||
title: output.agent_name,
|
||||
description: `${output.node_count} block${output.node_count === 1 ? "" : "s"}`,
|
||||
};
|
||||
}
|
||||
if (output.type === "clarification_needed") {
|
||||
return {
|
||||
badgeText: "Create agent",
|
||||
title: "Needs clarification",
|
||||
description: `${output.questions.length} question${output.questions.length === 1 ? "" : "s"}`,
|
||||
};
|
||||
}
|
||||
if (
|
||||
output.type === "operation_started" ||
|
||||
output.type === "operation_pending" ||
|
||||
output.type === "operation_in_progress"
|
||||
) {
|
||||
return { badgeText: "Create agent", title: "Creating agent" };
|
||||
}
|
||||
return { badgeText: "Create agent", title: "Error" };
|
||||
}
|
||||
|
||||
export function CreateAgentTool({ part }: Props) {
|
||||
const text = getAnimationText(part);
|
||||
const { onSend } = useCopilotChatActions();
|
||||
|
||||
const output = getCreateAgentToolOutput(part);
|
||||
const hasExpandableContent =
|
||||
part.state === "output-available" &&
|
||||
!!output &&
|
||||
(output.type === "operation_started" ||
|
||||
output.type === "operation_pending" ||
|
||||
output.type === "operation_in_progress" ||
|
||||
output.type === "agent_preview" ||
|
||||
output.type === "agent_saved" ||
|
||||
output.type === "clarification_needed" ||
|
||||
output.type === "error");
|
||||
|
||||
function handleClarificationAnswers(answers: Record<string, string>) {
|
||||
const contextMessage = Object.entries(answers)
|
||||
.map(([keyword, answer]) => `${keyword}: ${answer}`)
|
||||
.join("\n");
|
||||
|
||||
onSend(
|
||||
`I have the answers to your questions:\n\n${contextMessage}\n\nPlease proceed with creating the agent.`,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-2">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<StateIcon state={part.state} />
|
||||
<MorphingTextAnimation text={text} />
|
||||
</div>
|
||||
|
||||
{hasExpandableContent && output && (
|
||||
<ToolAccordion
|
||||
{...getAccordionMeta(output)}
|
||||
defaultExpanded={output.type === "clarification_needed"}
|
||||
>
|
||||
{(output.type === "operation_started" ||
|
||||
output.type === "operation_pending") && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Operation: {output.operation_id}
|
||||
</p>
|
||||
<p className="text-xs italic text-muted-foreground">
|
||||
Check your library in a few minutes.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "operation_in_progress" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
<p className="text-xs italic text-muted-foreground">
|
||||
Please wait for the current operation to finish.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "agent_saved" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Link
|
||||
href={output.library_agent_link}
|
||||
className="text-xs font-medium text-purple-600 hover:text-purple-700"
|
||||
>
|
||||
Open in library
|
||||
</Link>
|
||||
<Link
|
||||
href={output.agent_page_link}
|
||||
className="text-xs font-medium text-purple-600 hover:text-purple-700"
|
||||
>
|
||||
Open in builder
|
||||
</Link>
|
||||
</div>
|
||||
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
|
||||
{truncateText(
|
||||
formatMaybeJson({ agent_id: output.agent_id }),
|
||||
800,
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "agent_preview" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.description?.trim() && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{output.description}
|
||||
</p>
|
||||
)}
|
||||
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
|
||||
{truncateText(formatMaybeJson(output.agent_json), 1600)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "clarification_needed" && (
|
||||
<ClarificationQuestionsWidget
|
||||
questions={output.questions}
|
||||
message={output.message}
|
||||
onSubmitAnswers={handleClarificationAnswers}
|
||||
/>
|
||||
)}
|
||||
|
||||
{output.type === "error" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.error && (
|
||||
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
|
||||
{formatMaybeJson(output.error)}
|
||||
</pre>
|
||||
)}
|
||||
{output.details && (
|
||||
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
|
||||
{formatMaybeJson(output.details)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ToolAccordion>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import type { ToolUIPart } from "ai";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
CircleNotchIcon,
|
||||
XCircleIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
export interface ClarifyingQuestion {
|
||||
question: string;
|
||||
keyword: string;
|
||||
example?: string;
|
||||
}
|
||||
|
||||
export interface OperationStartedOutput {
|
||||
type: "operation_started";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
operation_id: string;
|
||||
tool_name: string;
|
||||
}
|
||||
|
||||
export interface OperationPendingOutput {
|
||||
type: "operation_pending";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
operation_id: string;
|
||||
tool_name: string;
|
||||
}
|
||||
|
||||
export interface OperationInProgressOutput {
|
||||
type: "operation_in_progress";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
tool_call_id: string;
|
||||
}
|
||||
|
||||
export interface AgentPreviewOutput {
|
||||
type: "agent_preview";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
agent_json: Record<string, unknown>;
|
||||
agent_name: string;
|
||||
description: string;
|
||||
node_count: number;
|
||||
link_count: number;
|
||||
}
|
||||
|
||||
export interface AgentSavedOutput {
|
||||
type: "agent_saved";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
agent_id: string;
|
||||
agent_name: string;
|
||||
library_agent_id: string;
|
||||
library_agent_link: string;
|
||||
agent_page_link: string;
|
||||
}
|
||||
|
||||
export interface ClarificationNeededOutput {
|
||||
type: "clarification_needed";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
questions: ClarifyingQuestion[];
|
||||
}
|
||||
|
||||
export interface ErrorOutput {
|
||||
type: "error";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
error?: string | null;
|
||||
details?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export type CreateAgentToolOutput =
|
||||
| OperationStartedOutput
|
||||
| OperationPendingOutput
|
||||
| OperationInProgressOutput
|
||||
| AgentPreviewOutput
|
||||
| AgentSavedOutput
|
||||
| ClarificationNeededOutput
|
||||
| ErrorOutput;
|
||||
|
||||
function parseOutput(output: unknown): CreateAgentToolOutput | null {
|
||||
if (!output) return null;
|
||||
if (typeof output === "string") {
|
||||
const trimmed = output.trim();
|
||||
if (!trimmed) return null;
|
||||
try {
|
||||
return JSON.parse(trimmed) as CreateAgentToolOutput;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (typeof output === "object") return output as CreateAgentToolOutput;
|
||||
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 getAnimationText(part: {
|
||||
state: ToolUIPart["state"];
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
}): string {
|
||||
switch (part.state) {
|
||||
case "input-streaming":
|
||||
return "Creating agent";
|
||||
case "input-available":
|
||||
return "Generating agent workflow";
|
||||
case "output-available": {
|
||||
const output = parseOutput(part.output);
|
||||
if (!output) return "Agent created";
|
||||
if (output.type === "operation_started") return "Agent creation started";
|
||||
if (output.type === "operation_pending")
|
||||
return "Agent creation in progress";
|
||||
if (output.type === "operation_in_progress")
|
||||
return "Agent creation already in progress";
|
||||
if (output.type === "agent_saved") return `Saved: ${output.agent_name}`;
|
||||
if (output.type === "agent_preview")
|
||||
return `Preview: ${output.agent_name}`;
|
||||
if (output.type === "clarification_needed") return "Needs clarification";
|
||||
return "Error creating agent";
|
||||
}
|
||||
case "output-error":
|
||||
return "Error creating agent";
|
||||
default:
|
||||
return "Processing";
|
||||
}
|
||||
}
|
||||
|
||||
export function StateIcon({ state }: { state: ToolUIPart["state"] }) {
|
||||
switch (state) {
|
||||
case "input-streaming":
|
||||
case "input-available":
|
||||
return (
|
||||
<CircleNotchIcon
|
||||
className="h-4 w-4 animate-spin text-muted-foreground"
|
||||
weight="bold"
|
||||
/>
|
||||
);
|
||||
case "output-available":
|
||||
return <CheckCircleIcon className="h-4 w-4 text-green-500" />;
|
||||
case "output-error":
|
||||
return <XCircleIcon className="h-4 w-4 text-red-500" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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()}…`;
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
"use client";
|
||||
|
||||
import type { ToolUIPart } from "ai";
|
||||
import Link from "next/link";
|
||||
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
|
||||
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
|
||||
import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions";
|
||||
import { ClarificationQuestionsWidget } from "@/components/contextual/Chat/components/ClarificationQuestionsWidget/ClarificationQuestionsWidget";
|
||||
import {
|
||||
formatMaybeJson,
|
||||
getAnimationText,
|
||||
getEditAgentToolOutput,
|
||||
StateIcon,
|
||||
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): {
|
||||
badgeText: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
} {
|
||||
if (output.type === "agent_saved") {
|
||||
return { badgeText: "Edit agent", title: output.agent_name };
|
||||
}
|
||||
if (output.type === "agent_preview") {
|
||||
return {
|
||||
badgeText: "Edit agent",
|
||||
title: output.agent_name,
|
||||
description: `${output.node_count} block${output.node_count === 1 ? "" : "s"}`,
|
||||
};
|
||||
}
|
||||
if (output.type === "clarification_needed") {
|
||||
return {
|
||||
badgeText: "Edit agent",
|
||||
title: "Needs clarification",
|
||||
description: `${output.questions.length} question${output.questions.length === 1 ? "" : "s"}`,
|
||||
};
|
||||
}
|
||||
if (
|
||||
output.type === "operation_started" ||
|
||||
output.type === "operation_pending" ||
|
||||
output.type === "operation_in_progress"
|
||||
) {
|
||||
return { badgeText: "Edit agent", title: "Editing agent" };
|
||||
}
|
||||
return { badgeText: "Edit agent", title: "Error" };
|
||||
}
|
||||
|
||||
export function EditAgentTool({ part }: Props) {
|
||||
const text = getAnimationText(part);
|
||||
const { onSend } = useCopilotChatActions();
|
||||
|
||||
const output = getEditAgentToolOutput(part);
|
||||
const hasExpandableContent =
|
||||
part.state === "output-available" &&
|
||||
!!output &&
|
||||
(output.type === "operation_started" ||
|
||||
output.type === "operation_pending" ||
|
||||
output.type === "operation_in_progress" ||
|
||||
output.type === "agent_preview" ||
|
||||
output.type === "agent_saved" ||
|
||||
output.type === "clarification_needed" ||
|
||||
output.type === "error");
|
||||
|
||||
function handleClarificationAnswers(answers: Record<string, string>) {
|
||||
const contextMessage = Object.entries(answers)
|
||||
.map(([keyword, answer]) => `${keyword}: ${answer}`)
|
||||
.join("\n");
|
||||
|
||||
onSend(
|
||||
`I have the answers to your questions:\n\n${contextMessage}\n\nPlease proceed with editing the agent.`,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-2">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<StateIcon state={part.state} />
|
||||
<MorphingTextAnimation text={text} />
|
||||
</div>
|
||||
|
||||
{hasExpandableContent && output && (
|
||||
<ToolAccordion
|
||||
{...getAccordionMeta(output)}
|
||||
defaultExpanded={output.type === "clarification_needed"}
|
||||
>
|
||||
{(output.type === "operation_started" ||
|
||||
output.type === "operation_pending") && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Operation: {output.operation_id}
|
||||
</p>
|
||||
<p className="text-xs italic text-muted-foreground">
|
||||
Check your library in a few minutes.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "operation_in_progress" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
<p className="text-xs italic text-muted-foreground">
|
||||
Please wait for the current operation to finish.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "agent_saved" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Link
|
||||
href={output.library_agent_link}
|
||||
className="text-xs font-medium text-purple-600 hover:text-purple-700"
|
||||
>
|
||||
Open in library
|
||||
</Link>
|
||||
<Link
|
||||
href={output.agent_page_link}
|
||||
className="text-xs font-medium text-purple-600 hover:text-purple-700"
|
||||
>
|
||||
Open in builder
|
||||
</Link>
|
||||
</div>
|
||||
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
|
||||
{truncateText(
|
||||
formatMaybeJson({ agent_id: output.agent_id }),
|
||||
800,
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "agent_preview" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.description?.trim() && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{output.description}
|
||||
</p>
|
||||
)}
|
||||
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
|
||||
{truncateText(formatMaybeJson(output.agent_json), 1600)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "clarification_needed" && (
|
||||
<ClarificationQuestionsWidget
|
||||
questions={output.questions}
|
||||
message={output.message}
|
||||
onSubmitAnswers={handleClarificationAnswers}
|
||||
/>
|
||||
)}
|
||||
|
||||
{output.type === "error" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
{output.error && (
|
||||
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
|
||||
{formatMaybeJson(output.error)}
|
||||
</pre>
|
||||
)}
|
||||
{output.details && (
|
||||
<pre className="whitespace-pre-wrap rounded-2xl border bg-muted/30 p-3 text-xs text-muted-foreground">
|
||||
{formatMaybeJson(output.details)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ToolAccordion>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import type { ToolUIPart } from "ai";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
CircleNotchIcon,
|
||||
XCircleIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
export interface ClarifyingQuestion {
|
||||
question: string;
|
||||
keyword: string;
|
||||
example?: string;
|
||||
}
|
||||
|
||||
export interface OperationStartedOutput {
|
||||
type: "operation_started";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
operation_id: string;
|
||||
tool_name: string;
|
||||
}
|
||||
|
||||
export interface OperationPendingOutput {
|
||||
type: "operation_pending";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
operation_id: string;
|
||||
tool_name: string;
|
||||
}
|
||||
|
||||
export interface OperationInProgressOutput {
|
||||
type: "operation_in_progress";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
tool_call_id: string;
|
||||
}
|
||||
|
||||
export interface AgentPreviewOutput {
|
||||
type: "agent_preview";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
agent_json: Record<string, unknown>;
|
||||
agent_name: string;
|
||||
description: string;
|
||||
node_count: number;
|
||||
link_count: number;
|
||||
}
|
||||
|
||||
export interface AgentSavedOutput {
|
||||
type: "agent_saved";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
agent_id: string;
|
||||
agent_name: string;
|
||||
library_agent_id: string;
|
||||
library_agent_link: string;
|
||||
agent_page_link: string;
|
||||
}
|
||||
|
||||
export interface ClarificationNeededOutput {
|
||||
type: "clarification_needed";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
questions: ClarifyingQuestion[];
|
||||
}
|
||||
|
||||
export interface ErrorOutput {
|
||||
type: "error";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
error?: string | null;
|
||||
details?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export type EditAgentToolOutput =
|
||||
| OperationStartedOutput
|
||||
| OperationPendingOutput
|
||||
| OperationInProgressOutput
|
||||
| AgentPreviewOutput
|
||||
| AgentSavedOutput
|
||||
| ClarificationNeededOutput
|
||||
| ErrorOutput;
|
||||
|
||||
function parseOutput(output: unknown): EditAgentToolOutput | null {
|
||||
if (!output) return null;
|
||||
if (typeof output === "string") {
|
||||
const trimmed = output.trim();
|
||||
if (!trimmed) return null;
|
||||
try {
|
||||
return JSON.parse(trimmed) as EditAgentToolOutput;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (typeof output === "object") return output as EditAgentToolOutput;
|
||||
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 getAnimationText(part: {
|
||||
state: ToolUIPart["state"];
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
}): string {
|
||||
switch (part.state) {
|
||||
case "input-streaming":
|
||||
return "Editing agent";
|
||||
case "input-available":
|
||||
return "Updating agent workflow";
|
||||
case "output-available": {
|
||||
const output = parseOutput(part.output);
|
||||
if (!output) return "Agent updated";
|
||||
if (output.type === "operation_started") return "Agent update started";
|
||||
if (output.type === "operation_pending")
|
||||
return "Agent update in progress";
|
||||
if (output.type === "operation_in_progress")
|
||||
return "Agent update already in progress";
|
||||
if (output.type === "agent_saved") return `Saved: ${output.agent_name}`;
|
||||
if (output.type === "agent_preview")
|
||||
return `Preview: ${output.agent_name}`;
|
||||
if (output.type === "clarification_needed") return "Needs clarification";
|
||||
return "Error editing agent";
|
||||
}
|
||||
case "output-error":
|
||||
return "Error editing agent";
|
||||
default:
|
||||
return "Processing";
|
||||
}
|
||||
}
|
||||
|
||||
export function StateIcon({ state }: { state: ToolUIPart["state"] }) {
|
||||
switch (state) {
|
||||
case "input-streaming":
|
||||
case "input-available":
|
||||
return (
|
||||
<CircleNotchIcon
|
||||
className="h-4 w-4 animate-spin text-muted-foreground"
|
||||
weight="bold"
|
||||
/>
|
||||
);
|
||||
case "output-available":
|
||||
return <CheckCircleIcon className="h-4 w-4 text-green-500" />;
|
||||
case "output-error":
|
||||
return <XCircleIcon className="h-4 w-4 text-red-500" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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()}…`;
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import type { ToolUIPart } from "ai";
|
||||
import Link from "next/link";
|
||||
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
|
||||
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
|
||||
import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions";
|
||||
import { ChatCredentialsSetup } from "@/components/contextual/Chat/components/ChatCredentialsSetup/ChatCredentialsSetup";
|
||||
import {
|
||||
formatMaybeJson,
|
||||
getAnimationText,
|
||||
@@ -37,6 +39,14 @@ function getAccordionMeta(output: RunAgentToolOutput): {
|
||||
};
|
||||
}
|
||||
|
||||
if (output.type === "agent_details") {
|
||||
return {
|
||||
badgeText: "Run agent",
|
||||
title: output.agent.name,
|
||||
description: "Inputs required",
|
||||
};
|
||||
}
|
||||
|
||||
if (output.type === "setup_requirements") {
|
||||
const missingCredsCount = Object.keys(
|
||||
output.setup_info.user_readiness.missing_credentials ?? {},
|
||||
@@ -60,16 +70,24 @@ function getAccordionMeta(output: RunAgentToolOutput): {
|
||||
|
||||
export function RunAgentTool({ part }: Props) {
|
||||
const text = getAnimationText(part);
|
||||
const { onSend } = useCopilotChatActions();
|
||||
|
||||
const output = getRunAgentToolOutput(part);
|
||||
const hasExpandableContent =
|
||||
part.state === "output-available" &&
|
||||
!!output &&
|
||||
(output.type === "execution_started" ||
|
||||
output.type === "agent_details" ||
|
||||
output.type === "setup_requirements" ||
|
||||
output.type === "need_login" ||
|
||||
output.type === "error");
|
||||
|
||||
function handleAllCredentialsComplete() {
|
||||
onSend(
|
||||
"I've configured the required credentials. Please check if everything is ready and proceed with running the agent.",
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-2">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
@@ -78,7 +96,13 @@ export function RunAgentTool({ part }: Props) {
|
||||
</div>
|
||||
|
||||
{hasExpandableContent && output && (
|
||||
<ToolAccordion {...getAccordionMeta(output)}>
|
||||
<ToolAccordion
|
||||
{...getAccordionMeta(output)}
|
||||
defaultExpanded={
|
||||
output.type === "setup_requirements" ||
|
||||
output.type === "agent_details"
|
||||
}
|
||||
>
|
||||
{output.type === "execution_started" && (
|
||||
<div className="grid gap-2">
|
||||
<div className="rounded-2xl border bg-background p-3">
|
||||
@@ -107,6 +131,28 @@ export function RunAgentTool({ part }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "agent_details" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
|
||||
{output.agent.description?.trim() && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{output.agent.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="rounded-2xl border bg-background p-3">
|
||||
<p className="text-xs font-medium text-foreground">Inputs</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Provide required inputs in chat, or ask to run with defaults.
|
||||
</p>
|
||||
<pre className="mt-2 whitespace-pre-wrap text-xs text-muted-foreground">
|
||||
{formatMaybeJson(output.agent.inputs)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{output.type === "setup_requirements" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
@@ -114,21 +160,24 @@ export function RunAgentTool({ part }: Props) {
|
||||
{Object.keys(
|
||||
output.setup_info.user_readiness.missing_credentials ?? {},
|
||||
).length > 0 && (
|
||||
<div className="rounded-2xl border bg-background p-3">
|
||||
<p className="text-xs font-medium text-foreground">
|
||||
Missing credentials
|
||||
</p>
|
||||
<ul className="mt-2 list-disc space-y-1 pl-5 text-xs text-muted-foreground">
|
||||
{Object.entries(
|
||||
output.setup_info.user_readiness.missing_credentials ??
|
||||
{},
|
||||
).map(([field, cred]) => (
|
||||
<li key={field}>
|
||||
{cred.title} ({cred.provider})
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<ChatCredentialsSetup
|
||||
credentials={Object.values(
|
||||
output.setup_info.user_readiness.missing_credentials ?? {},
|
||||
).map((cred) => ({
|
||||
provider: cred.provider,
|
||||
providerName:
|
||||
cred.provider_name ?? cred.provider.replace(/_/g, " "),
|
||||
credentialTypes: (cred.types ?? [cred.type]) as Array<
|
||||
"api_key" | "oauth2" | "user_password" | "host_scoped"
|
||||
>,
|
||||
title: cred.title,
|
||||
scopes: cred.scopes,
|
||||
}))}
|
||||
agentName={output.setup_info.agent_name}
|
||||
message={output.message}
|
||||
onAllCredentialsComplete={handleAllCredentialsComplete}
|
||||
onCancel={() => {}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{output.setup_info.requirements.inputs?.length > 0 && (
|
||||
|
||||
@@ -18,8 +18,11 @@ export interface RunAgentInput {
|
||||
export interface CredentialsMeta {
|
||||
id: string;
|
||||
provider: string;
|
||||
provider_name?: string;
|
||||
type: string;
|
||||
types?: string[];
|
||||
title: string;
|
||||
scopes?: string[];
|
||||
}
|
||||
|
||||
export interface SetupInfo {
|
||||
@@ -78,9 +81,31 @@ export interface NeedLoginOutput {
|
||||
session_id?: string;
|
||||
}
|
||||
|
||||
export interface AgentDetailsOutput {
|
||||
type: "agent_details";
|
||||
message: string;
|
||||
session_id?: string;
|
||||
agent: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
inputs: Record<string, unknown>;
|
||||
credentials: CredentialsMeta[];
|
||||
execution_options?: {
|
||||
manual?: boolean;
|
||||
scheduled?: boolean;
|
||||
webhook?: boolean;
|
||||
};
|
||||
};
|
||||
user_authenticated?: boolean;
|
||||
graph_id?: string | null;
|
||||
graph_version?: number | null;
|
||||
}
|
||||
|
||||
export type RunAgentToolOutput =
|
||||
| SetupRequirementsOutput
|
||||
| ExecutionStartedOutput
|
||||
| AgentDetailsOutput
|
||||
| NeedLoginOutput
|
||||
| ErrorOutput;
|
||||
|
||||
@@ -143,6 +168,9 @@ export function getAnimationText(part: {
|
||||
if (output.type === "execution_started") {
|
||||
return `Started: ${output.graph_name}`;
|
||||
}
|
||||
if (output.type === "agent_details") {
|
||||
return `Agent inputs: ${output.agent.name}`;
|
||||
}
|
||||
if (output.type === "setup_requirements") {
|
||||
return `Needs setup: ${output.setup_info.agent_name}`;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import type { ToolUIPart } from "ai";
|
||||
import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation";
|
||||
import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion";
|
||||
import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions";
|
||||
import { ChatCredentialsSetup } from "@/components/contextual/Chat/components/ChatCredentialsSetup/ChatCredentialsSetup";
|
||||
import {
|
||||
formatMaybeJson,
|
||||
getAnimationText,
|
||||
@@ -59,6 +61,7 @@ function getAccordionMeta(output: RunBlockToolOutput): {
|
||||
|
||||
export function RunBlockTool({ part }: Props) {
|
||||
const text = getAnimationText(part);
|
||||
const { onSend } = useCopilotChatActions();
|
||||
|
||||
const output = getRunBlockToolOutput(part);
|
||||
const hasExpandableContent =
|
||||
@@ -68,6 +71,12 @@ export function RunBlockTool({ part }: Props) {
|
||||
output.type === "setup_requirements" ||
|
||||
output.type === "error");
|
||||
|
||||
function handleAllCredentialsComplete() {
|
||||
onSend(
|
||||
"I've configured the required credentials. Please re-run the block now.",
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-2">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
@@ -76,7 +85,10 @@ export function RunBlockTool({ part }: Props) {
|
||||
</div>
|
||||
|
||||
{hasExpandableContent && output && (
|
||||
<ToolAccordion {...getAccordionMeta(output)}>
|
||||
<ToolAccordion
|
||||
{...getAccordionMeta(output)}
|
||||
defaultExpanded={output.type === "setup_requirements"}
|
||||
>
|
||||
{output.type === "block_output" && (
|
||||
<div className="grid gap-2">
|
||||
<p className="text-sm text-foreground">{output.message}</p>
|
||||
@@ -106,21 +118,24 @@ export function RunBlockTool({ part }: Props) {
|
||||
{Object.keys(
|
||||
output.setup_info.user_readiness.missing_credentials ?? {},
|
||||
).length > 0 && (
|
||||
<div className="rounded-2xl border bg-background p-3">
|
||||
<p className="text-xs font-medium text-foreground">
|
||||
Missing credentials
|
||||
</p>
|
||||
<ul className="mt-2 list-disc space-y-1 pl-5 text-xs text-muted-foreground">
|
||||
{Object.entries(
|
||||
output.setup_info.user_readiness.missing_credentials ??
|
||||
{},
|
||||
).map(([field, cred]) => (
|
||||
<li key={field}>
|
||||
{cred.title} ({cred.provider})
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<ChatCredentialsSetup
|
||||
credentials={Object.values(
|
||||
output.setup_info.user_readiness.missing_credentials ?? {},
|
||||
).map((cred) => ({
|
||||
provider: cred.provider,
|
||||
providerName:
|
||||
cred.provider_name ?? cred.provider.replace(/_/g, " "),
|
||||
credentialTypes: (cred.types ?? [cred.type]) as Array<
|
||||
"api_key" | "oauth2" | "user_password" | "host_scoped"
|
||||
>,
|
||||
title: cred.title,
|
||||
scopes: cred.scopes,
|
||||
}))}
|
||||
agentName={output.setup_info.agent_name}
|
||||
message={output.message}
|
||||
onAllCredentialsComplete={handleAllCredentialsComplete}
|
||||
onCancel={() => {}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{output.setup_info.requirements.inputs?.length > 0 && (
|
||||
|
||||
@@ -13,8 +13,11 @@ export interface RunBlockInput {
|
||||
export interface CredentialsMeta {
|
||||
id: string;
|
||||
provider: string;
|
||||
provider_name?: string;
|
||||
type: string;
|
||||
types?: string[];
|
||||
title: string;
|
||||
scopes?: string[];
|
||||
}
|
||||
|
||||
export interface SetupInfo {
|
||||
|
||||
Reference in New Issue
Block a user