mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-24 03:00:28 -05:00
Compare commits
9 Commits
chore/reac
...
fix/ui-for
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78baf1857b | ||
|
|
d08d5dd052 | ||
|
|
1bed2a2916 | ||
|
|
ef42b17e3b | ||
|
|
04dc25f110 | ||
|
|
0c0bd2ccb6 | ||
|
|
9a048b9caf | ||
|
|
5a3e33745e | ||
|
|
8ef89ac937 |
59
.github/workflows/platform-frontend-ci.yml
vendored
59
.github/workflows/platform-frontend-ci.yml
vendored
@@ -83,65 +83,6 @@ jobs:
|
||||
- name: Run lint
|
||||
run: pnpm lint
|
||||
|
||||
react-doctor:
|
||||
runs-on: ubuntu-latest
|
||||
needs: setup
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22.18.0"
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: autogpt_platform/frontend/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run React Doctor
|
||||
id: react-doctor
|
||||
continue-on-error: true
|
||||
run: |
|
||||
OUTPUT=$(pnpm react-doctor:diff 2>&1) || true
|
||||
echo "$OUTPUT"
|
||||
SCORE=$(echo "$OUTPUT" | grep -oP '\d+(?= / 100)' | head -1)
|
||||
echo "score=${SCORE:-0}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check React Doctor score
|
||||
env:
|
||||
RD_SCORE: ${{ steps.react-doctor.outputs.score }}
|
||||
MIN_SCORE: "90"
|
||||
run: |
|
||||
echo "React Doctor score: ${RD_SCORE}/100 (minimum: ${MIN_SCORE})"
|
||||
if [ "${RD_SCORE}" -lt "${MIN_SCORE}" ]; then
|
||||
echo "::error::React Doctor score ${RD_SCORE} is below the minimum threshold of ${MIN_SCORE}."
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " React Doctor score too low!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "To fix these issues, run Claude Code locally:"
|
||||
echo ""
|
||||
echo " cd autogpt_platform/frontend"
|
||||
echo " claude"
|
||||
echo ""
|
||||
echo "Then ask Claude to run react-doctor and fix the issues."
|
||||
echo "You can also run it manually:"
|
||||
echo ""
|
||||
echo " pnpm react-doctor # scan all files"
|
||||
echo " pnpm react-doctor:diff # scan only changed files"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chromatic:
|
||||
runs-on: ubuntu-latest
|
||||
needs: setup
|
||||
|
||||
@@ -23,8 +23,6 @@
|
||||
"build-storybook": "storybook build",
|
||||
"test-storybook": "test-storybook",
|
||||
"test-storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"pnpm run build-storybook -- --quiet && npx http-server storybook-static --port 6006 --silent\" \"wait-on tcp:6006 && pnpm run test-storybook\"",
|
||||
"react-doctor": "npx -y react-doctor@latest . --verbose",
|
||||
"react-doctor:diff": "npx -y react-doctor@latest . --verbose --diff",
|
||||
"generate:api": "npx --yes tsx ./scripts/generate-api-queries.ts && orval --config ./orval.config.ts",
|
||||
"generate:api:force": "npx --yes tsx ./scripts/generate-api-queries.ts --force && orval --config ./orval.config.ts"
|
||||
},
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
"use client";
|
||||
import { ReactFlowProvider } from "@xyflow/react";
|
||||
import { Flow } from "./components/FlowEditor/Flow/Flow";
|
||||
|
||||
export function BuilderContent() {
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
<ReactFlowProvider>
|
||||
<Flow />
|
||||
</ReactFlowProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import { beautifyString, cn } from "@/lib/utils";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { CustomNodeData } from "../CustomNode";
|
||||
import { NodeBadges } from "./NodeBadges";
|
||||
import { NodeContextMenu } from "./NodeContextMenu";
|
||||
@@ -25,9 +25,6 @@ export const NodeHeader = ({ data, nodeId }: Props) => {
|
||||
|
||||
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
||||
const [editedTitle, setEditedTitle] = useState(title);
|
||||
const titleInputRef = useCallback((node: HTMLInputElement | null) => {
|
||||
node?.focus();
|
||||
}, []);
|
||||
|
||||
const handleTitleEdit = () => {
|
||||
updateNodeData(nodeId, {
|
||||
@@ -55,10 +52,10 @@ export const NodeHeader = ({ data, nodeId }: Props) => {
|
||||
>
|
||||
{isEditingTitle ? (
|
||||
<input
|
||||
ref={titleInputRef}
|
||||
id="node-title-input"
|
||||
value={editedTitle}
|
||||
onChange={(e) => setEditedTitle(e.target.value)}
|
||||
autoFocus
|
||||
className={cn(
|
||||
"m-0 h-fit w-full border-none bg-transparent p-0 focus:outline-none focus:ring-0",
|
||||
"font-sans text-[1rem] font-semibold leading-[1.5rem] text-zinc-800",
|
||||
|
||||
@@ -300,6 +300,7 @@ export function MCPToolDialog({
|
||||
value={serverUrl}
|
||||
onChange={(e) => setServerUrl(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleDiscoverTools()}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -326,6 +327,7 @@ export function MCPToolDialog({
|
||||
value={manualToken}
|
||||
onChange={(e) => setManualToken(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleDiscoverTools()}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -52,7 +52,7 @@ export const HorizontalScroll: React.FC<HorizontalScrollAreaProps> = ({
|
||||
return;
|
||||
}
|
||||
const handleScroll = () => updateScrollState();
|
||||
element.addEventListener("scroll", handleScroll, { passive: true });
|
||||
element.addEventListener("scroll", handleScroll);
|
||||
window.addEventListener("resize", handleScroll);
|
||||
return () => {
|
||||
element.removeEventListener("scroll", handleScroll);
|
||||
|
||||
@@ -85,20 +85,12 @@ export const GraphSearchContent: React.FC<GraphSearchContentProps> = ({
|
||||
<Tooltip delayDuration={300}>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={`mx-4 my-2 flex h-20 cursor-pointer rounded-lg border border-zinc-200 bg-white ${
|
||||
index === selectedIndex
|
||||
? "border-zinc-400 shadow-md"
|
||||
: "hover:border-zinc-300 hover:shadow-sm"
|
||||
}`}
|
||||
onClick={() => onNodeSelect(node.id)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
onNodeSelect(node.id);
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setSelectedIndex(index);
|
||||
onNodeHover?.(node.id);
|
||||
|
||||
@@ -140,7 +140,10 @@ export function AgentRunDraftView({
|
||||
),
|
||||
[agentInputSchema],
|
||||
);
|
||||
const agentCredentialsInputFields = graph.credentials_input_schema.properties;
|
||||
const agentCredentialsInputFields = useMemo(
|
||||
() => graph.credentials_input_schema.properties,
|
||||
[graph],
|
||||
);
|
||||
const credentialFields = useMemo(
|
||||
function getCredentialFields() {
|
||||
return Object.entries(agentCredentialsInputFields);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { Metadata } from "next";
|
||||
import { BuilderContent } from "./BuilderContent";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Build",
|
||||
description: "Build your agent",
|
||||
};
|
||||
"use client";
|
||||
import { ReactFlowProvider } from "@xyflow/react";
|
||||
import { Flow } from "./components/FlowEditor/Flow/Flow";
|
||||
|
||||
export default function BuilderPage() {
|
||||
return <BuilderContent />;
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
<ReactFlowProvider>
|
||||
<Flow />
|
||||
</ReactFlowProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
import { ChatInput } from "@/app/(platform)/copilot/components/ChatInput/ChatInput";
|
||||
import { UIDataTypes, UIMessage, UITools } from "ai";
|
||||
import { LayoutGroup, LazyMotion, domAnimation, m } from "framer-motion";
|
||||
import { LayoutGroup, motion } from "framer-motion";
|
||||
import { ReactNode } from "react";
|
||||
import { ChatMessagesContainer } from "../ChatMessagesContainer/ChatMessagesContainer";
|
||||
import { CopilotChatActionsProvider } from "../CopilotChatActionsProvider/CopilotChatActionsProvider";
|
||||
@@ -38,47 +38,45 @@ export const ChatContainer = ({
|
||||
const inputLayoutId = "copilot-2-chat-input";
|
||||
|
||||
return (
|
||||
<LazyMotion features={domAnimation}>
|
||||
<CopilotChatActionsProvider onSend={onSend}>
|
||||
<LayoutGroup id="copilot-2-chat-layout">
|
||||
<div className="flex h-full min-h-0 w-full flex-col bg-[#f8f8f9] px-2 lg:px-0">
|
||||
{sessionId ? (
|
||||
<div className="mx-auto flex h-full min-h-0 w-full max-w-3xl flex-col">
|
||||
<ChatMessagesContainer
|
||||
messages={messages}
|
||||
status={status}
|
||||
error={error}
|
||||
isLoading={isLoadingSession}
|
||||
headerSlot={headerSlot}
|
||||
/>
|
||||
<m.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="relative px-3 pb-2 pt-2"
|
||||
>
|
||||
<div className="pointer-events-none absolute left-0 right-0 top-[-18px] z-10 h-6 bg-gradient-to-b from-transparent to-[#f8f8f9]" />
|
||||
<ChatInput
|
||||
inputId="chat-input-session"
|
||||
onSend={onSend}
|
||||
disabled={isBusy}
|
||||
isStreaming={isBusy}
|
||||
onStop={onStop}
|
||||
placeholder="What else can I help with?"
|
||||
/>
|
||||
</m.div>
|
||||
</div>
|
||||
) : (
|
||||
<EmptySession
|
||||
inputLayoutId={inputLayoutId}
|
||||
isCreatingSession={isCreatingSession}
|
||||
onCreateSession={onCreateSession}
|
||||
onSend={onSend}
|
||||
<CopilotChatActionsProvider onSend={onSend}>
|
||||
<LayoutGroup id="copilot-2-chat-layout">
|
||||
<div className="flex h-full min-h-0 w-full flex-col bg-[#f8f8f9] px-2 lg:px-0">
|
||||
{sessionId ? (
|
||||
<div className="mx-auto flex h-full min-h-0 w-full max-w-3xl flex-col">
|
||||
<ChatMessagesContainer
|
||||
messages={messages}
|
||||
status={status}
|
||||
error={error}
|
||||
isLoading={isLoadingSession}
|
||||
headerSlot={headerSlot}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</LayoutGroup>
|
||||
</CopilotChatActionsProvider>
|
||||
</LazyMotion>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="relative px-3 pb-2 pt-2"
|
||||
>
|
||||
<div className="pointer-events-none absolute left-0 right-0 top-[-18px] z-10 h-6 bg-gradient-to-b from-transparent to-[#f8f8f9]" />
|
||||
<ChatInput
|
||||
inputId="chat-input-session"
|
||||
onSend={onSend}
|
||||
disabled={isBusy}
|
||||
isStreaming={isBusy}
|
||||
onStop={onStop}
|
||||
placeholder="What else can I help with?"
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
) : (
|
||||
<EmptySession
|
||||
inputLayoutId={inputLayoutId}
|
||||
isCreatingSession={isCreatingSession}
|
||||
onCreateSession={onCreateSession}
|
||||
onSend={onSend}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</LayoutGroup>
|
||||
</CopilotChatActionsProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -117,7 +117,6 @@ export function AudioWaveform({
|
||||
{bars.map((height, i) => {
|
||||
const barHeight = Math.max(minBarHeight, height);
|
||||
return (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<div
|
||||
key={i}
|
||||
className="relative"
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
|
||||
import { toast } from "@/components/molecules/Toast/use-toast";
|
||||
import { ToolUIPart, UIDataTypes, UIMessage, UITools } from "ai";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { CreateAgentTool } from "../../tools/CreateAgent/CreateAgent";
|
||||
import { EditAgentTool } from "../../tools/EditAgent/EditAgent";
|
||||
@@ -58,7 +57,7 @@ function resolveWorkspaceUrls(text: string): string {
|
||||
* Falls back to <video> when an <img> fails to load for workspace files.
|
||||
*/
|
||||
function WorkspaceMediaImage(props: React.JSX.IntrinsicElements["img"]) {
|
||||
const { src, alt } = props;
|
||||
const { src, alt, ...rest } = props;
|
||||
const [imgFailed, setImgFailed] = useState(false);
|
||||
const isWorkspace = src?.includes("/workspace/files/") ?? false;
|
||||
|
||||
@@ -80,17 +79,16 @@ function WorkspaceMediaImage(props: React.JSX.IntrinsicElements["img"]) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={src}
|
||||
alt={alt || "Image"}
|
||||
width={0}
|
||||
height={0}
|
||||
sizes="100vw"
|
||||
className="h-auto w-full rounded-md border border-zinc-200"
|
||||
unoptimized
|
||||
className="h-auto max-w-full rounded-md border border-zinc-200"
|
||||
loading="lazy"
|
||||
onError={() => {
|
||||
if (isWorkspace) setImgFailed(true);
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -197,12 +195,12 @@ export const ChatMessagesContainer = ({
|
||||
"group-[.is-assistant]:bg-transparent group-[.is-assistant]:text-slate-900"
|
||||
}
|
||||
>
|
||||
{message.parts.map((part) => {
|
||||
{message.parts.map((part, i) => {
|
||||
switch (part.type) {
|
||||
case "text":
|
||||
return (
|
||||
<MessageResponse
|
||||
key={`${message.id}-text`}
|
||||
key={`${message.id}-${i}`}
|
||||
components={STREAMDOWN_COMPONENTS}
|
||||
>
|
||||
{resolveWorkspaceUrls(part.text)}
|
||||
@@ -211,7 +209,7 @@ export const ChatMessagesContainer = ({
|
||||
case "tool-find_block":
|
||||
return (
|
||||
<FindBlocksTool
|
||||
key={(part as ToolUIPart).toolCallId}
|
||||
key={`${message.id}-${i}`}
|
||||
part={part as ToolUIPart}
|
||||
/>
|
||||
);
|
||||
@@ -219,7 +217,7 @@ export const ChatMessagesContainer = ({
|
||||
case "tool-find_library_agent":
|
||||
return (
|
||||
<FindAgentsTool
|
||||
key={(part as ToolUIPart).toolCallId}
|
||||
key={`${message.id}-${i}`}
|
||||
part={part as ToolUIPart}
|
||||
/>
|
||||
);
|
||||
@@ -227,14 +225,14 @@ export const ChatMessagesContainer = ({
|
||||
case "tool-get_doc_page":
|
||||
return (
|
||||
<SearchDocsTool
|
||||
key={(part as ToolUIPart).toolCallId}
|
||||
key={`${message.id}-${i}`}
|
||||
part={part as ToolUIPart}
|
||||
/>
|
||||
);
|
||||
case "tool-run_block":
|
||||
return (
|
||||
<RunBlockTool
|
||||
key={(part as ToolUIPart).toolCallId}
|
||||
key={`${message.id}-${i}`}
|
||||
part={part as ToolUIPart}
|
||||
/>
|
||||
);
|
||||
@@ -242,42 +240,42 @@ export const ChatMessagesContainer = ({
|
||||
case "tool-schedule_agent":
|
||||
return (
|
||||
<RunAgentTool
|
||||
key={(part as ToolUIPart).toolCallId}
|
||||
key={`${message.id}-${i}`}
|
||||
part={part as ToolUIPart}
|
||||
/>
|
||||
);
|
||||
case "tool-create_agent":
|
||||
return (
|
||||
<CreateAgentTool
|
||||
key={(part as ToolUIPart).toolCallId}
|
||||
key={`${message.id}-${i}`}
|
||||
part={part as ToolUIPart}
|
||||
/>
|
||||
);
|
||||
case "tool-edit_agent":
|
||||
return (
|
||||
<EditAgentTool
|
||||
key={(part as ToolUIPart).toolCallId}
|
||||
key={`${message.id}-${i}`}
|
||||
part={part as ToolUIPart}
|
||||
/>
|
||||
);
|
||||
case "tool-view_agent_output":
|
||||
return (
|
||||
<ViewAgentOutputTool
|
||||
key={(part as ToolUIPart).toolCallId}
|
||||
key={`${message.id}-${i}`}
|
||||
part={part as ToolUIPart}
|
||||
/>
|
||||
);
|
||||
case "tool-search_feature_requests":
|
||||
return (
|
||||
<SearchFeatureRequestsTool
|
||||
key={(part as ToolUIPart).toolCallId}
|
||||
key={`${message.id}-${i}`}
|
||||
part={part as ToolUIPart}
|
||||
/>
|
||||
);
|
||||
case "tool-create_feature_request":
|
||||
return (
|
||||
<CreateFeatureRequestTool
|
||||
key={(part as ToolUIPart).toolCallId}
|
||||
key={`${message.id}-${i}`}
|
||||
part={part as ToolUIPart}
|
||||
/>
|
||||
);
|
||||
@@ -287,7 +285,7 @@ export const ChatMessagesContainer = ({
|
||||
if (part.type.startsWith("tool-")) {
|
||||
return (
|
||||
<GenericTool
|
||||
key={(part as ToolUIPart).toolCallId}
|
||||
key={`${message.id}-${i}`}
|
||||
part={part as ToolUIPart}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { DotsThree, PlusCircleIcon, PlusIcon } from "@phosphor-icons/react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { LazyMotion, domAnimation, m } from "framer-motion";
|
||||
import { motion } from "framer-motion";
|
||||
import { parseAsString, useQueryState } from "nuqs";
|
||||
import { useState } from "react";
|
||||
import { DeleteChatDialog } from "../DeleteChatDialog/DeleteChatDialog";
|
||||
@@ -129,7 +129,7 @@ export function ChatSidebar() {
|
||||
}
|
||||
|
||||
return (
|
||||
<LazyMotion features={domAnimation}>
|
||||
<>
|
||||
<Sidebar
|
||||
variant="inset"
|
||||
collapsible="icon"
|
||||
@@ -144,7 +144,7 @@ export function ChatSidebar() {
|
||||
: "flex-row items-center justify-between",
|
||||
)}
|
||||
>
|
||||
<m.div
|
||||
<motion.div
|
||||
key={isCollapsed ? "header-collapsed" : "header-expanded"}
|
||||
className="flex flex-col items-center gap-3 pt-4"
|
||||
initial={{ opacity: 0, filter: "blur(3px)" }}
|
||||
@@ -162,12 +162,12 @@ export function ChatSidebar() {
|
||||
<span className="sr-only">New Chat</span>
|
||||
</Button>
|
||||
</div>
|
||||
</m.div>
|
||||
</motion.div>
|
||||
</SidebarHeader>
|
||||
)}
|
||||
<SidebarContent className="gap-4 overflow-y-auto px-4 py-4 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||
{!isCollapsed && (
|
||||
<m.div
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2, delay: 0.1 }}
|
||||
@@ -179,11 +179,11 @@ export function ChatSidebar() {
|
||||
<div className="relative left-6">
|
||||
<SidebarTrigger />
|
||||
</div>
|
||||
</m.div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{!isCollapsed && (
|
||||
<m.div
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2, delay: 0.15 }}
|
||||
@@ -256,12 +256,12 @@ export function ChatSidebar() {
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</m.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</SidebarContent>
|
||||
{!isCollapsed && sessionId && (
|
||||
<SidebarFooter className="shrink-0 bg-zinc-50 p-3 pb-1 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)]">
|
||||
<m.div
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2, delay: 0.2 }}
|
||||
@@ -275,7 +275,7 @@ export function ChatSidebar() {
|
||||
>
|
||||
New Chat
|
||||
</Button>
|
||||
</m.div>
|
||||
</motion.div>
|
||||
</SidebarFooter>
|
||||
)}
|
||||
</Sidebar>
|
||||
@@ -286,6 +286,6 @@ export function ChatSidebar() {
|
||||
onConfirm={handleConfirmDelete}
|
||||
onCancel={handleCancelDelete}
|
||||
/>
|
||||
</LazyMotion>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { SpinnerGapIcon } from "@phosphor-icons/react";
|
||||
import { LazyMotion, domAnimation, m } from "framer-motion";
|
||||
import { motion } from "framer-motion";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
getGreetingName,
|
||||
@@ -29,7 +29,7 @@ export function EmptySession({
|
||||
const greetingName = getGreetingName(user);
|
||||
const quickActions = getQuickActions();
|
||||
const [loadingAction, setLoadingAction] = useState<string | null>(null);
|
||||
const [inputPlaceholder, setInputPlaceholder] = useState(() =>
|
||||
const [inputPlaceholder, setInputPlaceholder] = useState(
|
||||
getInputPlaceholder(),
|
||||
);
|
||||
|
||||
@@ -49,65 +49,63 @@ export function EmptySession({
|
||||
}
|
||||
|
||||
return (
|
||||
<LazyMotion features={domAnimation}>
|
||||
<div className="flex h-full flex-1 items-center justify-center overflow-y-auto bg-[#f8f8f9] px-0 py-5 md:px-6 md:py-10">
|
||||
<m.div
|
||||
className="w-full max-w-3xl text-center"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<Text variant="h3" className="mb-1 !text-[1.375rem] text-zinc-700">
|
||||
Hey, <span className="text-violet-600">{greetingName}</span>
|
||||
</Text>
|
||||
<Text variant="h3" className="mb-8 !font-normal">
|
||||
Tell me about your work — I'll find what to automate.
|
||||
</Text>
|
||||
<div className="flex h-full flex-1 items-center justify-center overflow-y-auto bg-[#f8f8f9] px-0 py-5 md:px-6 md:py-10">
|
||||
<motion.div
|
||||
className="w-full max-w-3xl text-center"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<Text variant="h3" className="mb-1 !text-[1.375rem] text-zinc-700">
|
||||
Hey, <span className="text-violet-600">{greetingName}</span>
|
||||
</Text>
|
||||
<Text variant="h3" className="mb-8 !font-normal">
|
||||
Tell me about your work — I'll find what to automate.
|
||||
</Text>
|
||||
|
||||
<div className="mb-6">
|
||||
<m.div
|
||||
layoutId={inputLayoutId}
|
||||
transition={{ type: "spring", bounce: 0.2, duration: 0.65 }}
|
||||
className="w-full px-2"
|
||||
>
|
||||
<ChatInput
|
||||
inputId="chat-input-empty"
|
||||
onSend={onSend}
|
||||
disabled={isCreatingSession}
|
||||
placeholder={inputPlaceholder}
|
||||
className="w-full"
|
||||
/>
|
||||
</m.div>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<motion.div
|
||||
layoutId={inputLayoutId}
|
||||
transition={{ type: "spring", bounce: 0.2, duration: 0.65 }}
|
||||
className="w-full px-2"
|
||||
>
|
||||
<ChatInput
|
||||
inputId="chat-input-empty"
|
||||
onSend={onSend}
|
||||
disabled={isCreatingSession}
|
||||
placeholder={inputPlaceholder}
|
||||
className="w-full"
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-center gap-3 overflow-x-auto [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||
{quickActions.map((action) => (
|
||||
<Button
|
||||
key={action}
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="small"
|
||||
onClick={() => void handleQuickActionClick(action)}
|
||||
disabled={isCreatingSession || loadingAction !== null}
|
||||
aria-busy={loadingAction === action}
|
||||
leftIcon={
|
||||
loadingAction === action ? (
|
||||
<SpinnerGapIcon
|
||||
className="h-4 w-4 animate-spin"
|
||||
weight="bold"
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
className="h-auto shrink-0 border-zinc-300 px-3 py-2 text-[.9rem] text-zinc-600"
|
||||
>
|
||||
{action}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</m.div>
|
||||
</div>
|
||||
</LazyMotion>
|
||||
<div className="flex flex-wrap items-center justify-center gap-3 overflow-x-auto [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||
{quickActions.map((action) => (
|
||||
<Button
|
||||
key={action}
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="small"
|
||||
onClick={() => void handleQuickActionClick(action)}
|
||||
disabled={isCreatingSession || loadingAction !== null}
|
||||
aria-busy={loadingAction === action}
|
||||
leftIcon={
|
||||
loadingAction === action ? (
|
||||
<SpinnerGapIcon
|
||||
className="h-4 w-4 animate-spin"
|
||||
weight="bold"
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
className="h-auto shrink-0 border-zinc-300 px-3 py-2 text-[.9rem] text-zinc-600"
|
||||
>
|
||||
{action}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AnimatePresence, LazyMotion, domAnimation, m } from "framer-motion";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
@@ -10,47 +10,45 @@ export function MorphingTextAnimation({ text, className }: Props) {
|
||||
const letters = text.split("");
|
||||
|
||||
return (
|
||||
<LazyMotion features={domAnimation}>
|
||||
<div className={cn(className)}>
|
||||
<AnimatePresence mode="popLayout" initial={false}>
|
||||
<m.div key={text} className="whitespace-nowrap">
|
||||
<m.span className="inline-flex overflow-hidden">
|
||||
{letters.map((char, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<m.span
|
||||
key={`${text}-${index}`}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 8,
|
||||
rotateX: "80deg",
|
||||
filter: "blur(6px)",
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
rotateX: "0deg",
|
||||
filter: "blur(0px)",
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
y: -8,
|
||||
rotateX: "-80deg",
|
||||
filter: "blur(6px)",
|
||||
}}
|
||||
transition={{
|
||||
delay: 0.015 * index,
|
||||
type: "spring",
|
||||
bounce: 0.5,
|
||||
}}
|
||||
className="inline-block"
|
||||
>
|
||||
{char === " " ? "\u00A0" : char}
|
||||
</m.span>
|
||||
))}
|
||||
</m.span>
|
||||
</m.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</LazyMotion>
|
||||
<div className={cn(className)}>
|
||||
<AnimatePresence mode="popLayout" initial={false}>
|
||||
<motion.div key={text} className="whitespace-nowrap">
|
||||
<motion.span className="inline-flex overflow-hidden">
|
||||
{letters.map((char, index) => (
|
||||
<motion.span
|
||||
key={`${text}-${index}`}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 8,
|
||||
rotateX: "80deg",
|
||||
filter: "blur(6px)",
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
rotateX: "0deg",
|
||||
filter: "blur(0px)",
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
y: -8,
|
||||
rotateX: "-80deg",
|
||||
filter: "blur(6px)",
|
||||
}}
|
||||
style={{ willChange: "transform" }}
|
||||
transition={{
|
||||
delay: 0.015 * index,
|
||||
type: "spring",
|
||||
bounce: 0.5,
|
||||
}}
|
||||
className="inline-block"
|
||||
>
|
||||
{char === " " ? "\u00A0" : char}
|
||||
</motion.span>
|
||||
))}
|
||||
</motion.span>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,13 +2,7 @@
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CaretDownIcon } from "@phosphor-icons/react";
|
||||
import {
|
||||
AnimatePresence,
|
||||
LazyMotion,
|
||||
domAnimation,
|
||||
m,
|
||||
useReducedMotion,
|
||||
} from "framer-motion";
|
||||
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
|
||||
import { useId } from "react";
|
||||
import { useToolAccordion } from "./useToolAccordion";
|
||||
|
||||
@@ -44,66 +38,65 @@ export function ToolAccordion({
|
||||
});
|
||||
|
||||
return (
|
||||
<LazyMotion features={domAnimation}>
|
||||
<div
|
||||
className={cn(
|
||||
"mt-2 w-full rounded-lg border border-slate-200 bg-slate-100 px-3 py-2",
|
||||
className,
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"mt-2 w-full rounded-lg border border-slate-200 bg-slate-100 px-3 py-2",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={isExpanded}
|
||||
aria-controls={contentId}
|
||||
onClick={toggle}
|
||||
className="flex w-full items-center justify-between gap-3 py-1 text-left"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={isExpanded}
|
||||
aria-controls={contentId}
|
||||
onClick={toggle}
|
||||
className="flex w-full items-center justify-between gap-3 py-1 text-left"
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<span className="flex shrink-0 items-center text-gray-800">
|
||||
{icon}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p
|
||||
className={cn(
|
||||
"truncate text-sm font-medium text-gray-800",
|
||||
titleClassName,
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
{description && (
|
||||
<p className="truncate text-xs text-slate-800">{description}</p>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<span className="flex shrink-0 items-center text-gray-800">
|
||||
{icon}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p
|
||||
className={cn(
|
||||
"truncate text-sm font-medium text-gray-800",
|
||||
titleClassName,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<CaretDownIcon
|
||||
className={cn(
|
||||
"h-4 w-4 shrink-0 text-slate-500 transition-transform",
|
||||
isExpanded && "rotate-180",
|
||||
)}
|
||||
weight="bold"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<AnimatePresence initial={false}>
|
||||
{isExpanded && (
|
||||
<m.div
|
||||
id={contentId}
|
||||
initial={{ height: 0, opacity: 0, filter: "blur(10px)" }}
|
||||
animate={{ height: "auto", opacity: 1, filter: "blur(0px)" }}
|
||||
exit={{ height: 0, opacity: 0, filter: "blur(10px)" }}
|
||||
transition={
|
||||
shouldReduceMotion
|
||||
? { duration: 0 }
|
||||
: { type: "spring", bounce: 0.35, duration: 0.55 }
|
||||
}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="pb-2 pt-3">{children}</div>
|
||||
</m.div>
|
||||
{title}
|
||||
</p>
|
||||
{description && (
|
||||
<p className="truncate text-xs text-slate-800">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<CaretDownIcon
|
||||
className={cn(
|
||||
"h-4 w-4 shrink-0 text-slate-500 transition-transform",
|
||||
isExpanded && "rotate-180",
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</LazyMotion>
|
||||
weight="bold"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<AnimatePresence initial={false}>
|
||||
{isExpanded && (
|
||||
<motion.div
|
||||
id={contentId}
|
||||
initial={{ height: 0, opacity: 0, filter: "blur(10px)" }}
|
||||
animate={{ height: "auto", opacity: 1, filter: "blur(0px)" }}
|
||||
exit={{ height: 0, opacity: 0, filter: "blur(10px)" }}
|
||||
transition={
|
||||
shouldReduceMotion
|
||||
? { duration: 0 }
|
||||
: { type: "spring", bounce: 0.35, duration: 0.55 }
|
||||
}
|
||||
className="overflow-hidden"
|
||||
style={{ willChange: "height, opacity, filter" }}
|
||||
>
|
||||
<div className="pb-2 pt-3">{children}</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Copilot",
|
||||
description: "Chat with your AI copilot",
|
||||
};
|
||||
|
||||
export default function CopilotLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Copilot Styleguide",
|
||||
description: "Copilot UI component styleguide",
|
||||
};
|
||||
|
||||
export default function StyleguideLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return children;
|
||||
}
|
||||
@@ -161,7 +161,7 @@ export function ClarificationQuestionsCard({
|
||||
|
||||
return (
|
||||
<div
|
||||
key={q.keyword}
|
||||
key={`${q.keyword}-${index}`}
|
||||
className={cn(
|
||||
"relative rounded-lg border p-3",
|
||||
isAnswered
|
||||
|
||||
@@ -501,27 +501,79 @@ function getFileAccordionData(
|
||||
"path",
|
||||
"pattern",
|
||||
) ?? "File";
|
||||
const content = getStringField(output, "content", "text", "_raw");
|
||||
const content = getStringField(
|
||||
output,
|
||||
"content",
|
||||
"text",
|
||||
"preview",
|
||||
"content_preview",
|
||||
"_raw",
|
||||
);
|
||||
const message = getStringField(output, "message");
|
||||
|
||||
// Handle base64 content from workspace files
|
||||
let displayContent = content;
|
||||
if (output.content_base64 && typeof output.content_base64 === "string") {
|
||||
try {
|
||||
const bytes = Uint8Array.from(atob(output.content_base64), (c) =>
|
||||
c.charCodeAt(0),
|
||||
);
|
||||
displayContent = new TextDecoder().decode(bytes);
|
||||
} catch {
|
||||
displayContent = "[Binary content]";
|
||||
}
|
||||
}
|
||||
|
||||
// Handle MCP-style content blocks from SDK tools (Read, Glob, Grep, Edit)
|
||||
if (!displayContent) {
|
||||
displayContent = extractMcpText(output);
|
||||
}
|
||||
|
||||
// For Glob/list results, try to show file list
|
||||
const files = Array.isArray(output.files)
|
||||
? output.files.filter((f: unknown): f is string => typeof f === "string")
|
||||
: null;
|
||||
// Files can be either strings (from Glob) or objects (from list_workspace_files)
|
||||
const files = Array.isArray(output.files) ? output.files : null;
|
||||
|
||||
// Format file list for display
|
||||
let fileListText: string | null = null;
|
||||
if (files && files.length > 0) {
|
||||
const fileLines = files.map((f: unknown) => {
|
||||
if (typeof f === "string") {
|
||||
return f;
|
||||
}
|
||||
if (typeof f === "object" && f !== null) {
|
||||
const fileObj = f as Record<string, unknown>;
|
||||
// Workspace file format: path (size, mime_type)
|
||||
const filePath =
|
||||
typeof fileObj.path === "string"
|
||||
? fileObj.path
|
||||
: typeof fileObj.name === "string"
|
||||
? fileObj.name
|
||||
: "unknown";
|
||||
const mimeType =
|
||||
typeof fileObj.mime_type === "string" ? fileObj.mime_type : "unknown";
|
||||
const size =
|
||||
typeof fileObj.size_bytes === "number"
|
||||
? ` (${(fileObj.size_bytes / 1024).toFixed(1)} KB, ${mimeType})`
|
||||
: "";
|
||||
return `${filePath}${size}`;
|
||||
}
|
||||
return String(f);
|
||||
});
|
||||
fileListText = fileLines.join("\n");
|
||||
}
|
||||
|
||||
return {
|
||||
title: message ?? "File output",
|
||||
description: truncate(filePath, 80),
|
||||
content: (
|
||||
<div className="space-y-2">
|
||||
{content && (
|
||||
<ContentCodeBlock>{truncate(content, 2000)}</ContentCodeBlock>
|
||||
{displayContent && (
|
||||
<ContentCodeBlock>{truncate(displayContent, 2000)}</ContentCodeBlock>
|
||||
)}
|
||||
{files && files.length > 0 && (
|
||||
<ContentCodeBlock>
|
||||
{truncate(files.join("\n"), 2000)}
|
||||
</ContentCodeBlock>
|
||||
{fileListText && (
|
||||
<ContentCodeBlock>{truncate(fileListText, 2000)}</ContentCodeBlock>
|
||||
)}
|
||||
{!content && !files && message && (
|
||||
{!displayContent && !fileListText && message && (
|
||||
<ContentMessage>{message}</ContentMessage>
|
||||
)}
|
||||
</div>
|
||||
@@ -557,11 +609,8 @@ function getTodoAccordionData(input: unknown): AccordionData {
|
||||
description: `${completed}/${total} completed`,
|
||||
content: (
|
||||
<div className="space-y-1 py-1">
|
||||
{todos.map((todo, idx) => (
|
||||
<div
|
||||
key={`${todo.status}:${todo.content}:${idx}`}
|
||||
className="flex items-start gap-2 text-xs"
|
||||
>
|
||||
{todos.map((todo, i) => (
|
||||
<div key={i} className="flex items-start gap-2 text-xs">
|
||||
<span className="mt-0.5 flex-shrink-0">
|
||||
{todo.status === "completed" ? (
|
||||
<CheckCircleIcon
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { AgentDetailsResponse } from "@/app/api/__generated__/models/agentD
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { FormRenderer } from "@/components/renderers/InputRenderer/FormRenderer";
|
||||
import { AnimatePresence, LazyMotion, domAnimation, m } from "framer-motion";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import { useCopilotChatActions } from "../../../../components/CopilotChatActionsProvider/useCopilotChatActions";
|
||||
import { ContentMessage } from "../../../../components/ToolAccordion/AccordionContent";
|
||||
@@ -39,83 +39,78 @@ export function AgentDetailsCard({ output }: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<LazyMotion features={domAnimation}>
|
||||
<div className="grid gap-2">
|
||||
<ContentMessage>
|
||||
Run this agent with example values or your own inputs.
|
||||
</ContentMessage>
|
||||
<div className="grid gap-2">
|
||||
<ContentMessage>
|
||||
Run this agent with example values or your own inputs.
|
||||
</ContentMessage>
|
||||
|
||||
<div className="flex gap-2 pt-4">
|
||||
<Button
|
||||
size="small"
|
||||
className="w-fit"
|
||||
onClick={handleRunWithExamples}
|
||||
>
|
||||
Run with example values
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="small"
|
||||
className="w-fit"
|
||||
onClick={() => setShowInputForm((prev) => !prev)}
|
||||
>
|
||||
Run with my inputs
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<AnimatePresence initial={false}>
|
||||
{showInputForm && buildInputSchema(output.agent.inputs) && (
|
||||
<m.div
|
||||
initial={{ height: 0, opacity: 0, filter: "blur(6px)" }}
|
||||
animate={{ height: "auto", opacity: 1, filter: "blur(0px)" }}
|
||||
exit={{ height: 0, opacity: 0, filter: "blur(6px)" }}
|
||||
transition={{
|
||||
height: { type: "spring", bounce: 0.15, duration: 0.5 },
|
||||
opacity: { duration: 0.25 },
|
||||
filter: { duration: 0.2 },
|
||||
}}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="mt-4 rounded-2xl border bg-background p-3 pt-4">
|
||||
<Text variant="body-medium">Enter your inputs</Text>
|
||||
<FormRenderer
|
||||
jsonSchema={buildInputSchema(output.agent.inputs)!}
|
||||
handleChange={(v) => setInputValues(v.formData ?? {})}
|
||||
uiSchema={{
|
||||
"ui:submitButtonOptions": { norender: true },
|
||||
}}
|
||||
initialValues={inputValues}
|
||||
formContext={{
|
||||
showHandles: false,
|
||||
size: "small",
|
||||
}}
|
||||
/>
|
||||
<div className="-mt-8 flex gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
className="w-fit"
|
||||
onClick={handleRunWithInputs}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
className="w-fit"
|
||||
onClick={() => {
|
||||
setShowInputForm(false);
|
||||
setInputValues({});
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</m.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<div className="flex gap-2 pt-4">
|
||||
<Button size="small" className="w-fit" onClick={handleRunWithExamples}>
|
||||
Run with example values
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="small"
|
||||
className="w-fit"
|
||||
onClick={() => setShowInputForm((prev) => !prev)}
|
||||
>
|
||||
Run with my inputs
|
||||
</Button>
|
||||
</div>
|
||||
</LazyMotion>
|
||||
|
||||
<AnimatePresence initial={false}>
|
||||
{showInputForm && buildInputSchema(output.agent.inputs) && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0, filter: "blur(6px)" }}
|
||||
animate={{ height: "auto", opacity: 1, filter: "blur(0px)" }}
|
||||
exit={{ height: 0, opacity: 0, filter: "blur(6px)" }}
|
||||
transition={{
|
||||
height: { type: "spring", bounce: 0.15, duration: 0.5 },
|
||||
opacity: { duration: 0.25 },
|
||||
filter: { duration: 0.2 },
|
||||
}}
|
||||
className="overflow-hidden"
|
||||
style={{ willChange: "height, opacity, filter" }}
|
||||
>
|
||||
<div className="mt-4 rounded-2xl border bg-background p-3 pt-4">
|
||||
<Text variant="body-medium">Enter your inputs</Text>
|
||||
<FormRenderer
|
||||
jsonSchema={buildInputSchema(output.agent.inputs)!}
|
||||
handleChange={(v) => setInputValues(v.formData ?? {})}
|
||||
uiSchema={{
|
||||
"ui:submitButtonOptions": { norender: true },
|
||||
}}
|
||||
initialValues={inputValues}
|
||||
formContext={{
|
||||
showHandles: false,
|
||||
size: "small",
|
||||
}}
|
||||
/>
|
||||
<div className="-mt-8 flex gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
className="w-fit"
|
||||
onClick={handleRunWithInputs}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
className="w-fit"
|
||||
onClick={() => {
|
||||
setShowInputForm(false);
|
||||
setInputValues({});
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ function OutputKeySection({
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
{visibleItems.map((item, i) => (
|
||||
<RenderOutputValue key={`${outputKey}-${i}`} value={item} />
|
||||
<RenderOutputValue key={i} value={item} />
|
||||
))}
|
||||
</div>
|
||||
{hasMoreItems && (
|
||||
|
||||
@@ -209,10 +209,7 @@ export function ViewAgentOutputTool({ part }: Props) {
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
{items.slice(0, 3).map((item, i) => (
|
||||
<RenderOutputValue
|
||||
key={`${key}-${i}`}
|
||||
value={item}
|
||||
/>
|
||||
<RenderOutputValue key={i} value={item} />
|
||||
))}
|
||||
</div>
|
||||
</ContentCard>
|
||||
|
||||
@@ -23,23 +23,13 @@ export function SidebarItemCard({
|
||||
onClick,
|
||||
actions,
|
||||
}: Props) {
|
||||
function handleKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
onClick?.();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={cn(
|
||||
"w-full cursor-pointer rounded-large border border-zinc-200 bg-white p-3 text-left ring-1 ring-transparent transition-all duration-150 hover:scale-[1.01] hover:bg-slate-50/50",
|
||||
selected ? "border-slate-800 ring-slate-800" : undefined,
|
||||
)}
|
||||
onClick={onClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<div className="flex min-w-0 items-center justify-start gap-3">
|
||||
{icon}
|
||||
@@ -59,13 +49,7 @@ export function SidebarItemCard({
|
||||
</Text>
|
||||
</div>
|
||||
{actions ? (
|
||||
<div
|
||||
role="presentation"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{actions}
|
||||
</div>
|
||||
<div onClick={(e) => e.stopPropagation()}>{actions}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { redirect } from "next/navigation";
|
||||
"use client";
|
||||
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "AutoGPT Platform",
|
||||
description: "AutoGPT Platform",
|
||||
};
|
||||
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function Page() {
|
||||
redirect("/copilot");
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
router.replace("/copilot");
|
||||
}, [router]);
|
||||
|
||||
return <LoadingSpinner size="large" cover />;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
const getYouTubeVideoId = (url: string) => {
|
||||
const regExp =
|
||||
@@ -77,7 +76,6 @@ const VideoRenderer: React.FC<{ videoUrl: string }> = ({ videoUrl }) => {
|
||||
width="100%"
|
||||
height="315"
|
||||
src={`https://www.youtube.com/embed/${videoId}`}
|
||||
title="Embedded content"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
@@ -94,15 +92,15 @@ const VideoRenderer: React.FC<{ videoUrl: string }> = ({ videoUrl }) => {
|
||||
const ImageRenderer: React.FC<{ imageUrl: string }> = ({ imageUrl }) => {
|
||||
return (
|
||||
<div className="w-full p-2">
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt="Image"
|
||||
width={0}
|
||||
height={0}
|
||||
sizes="100vw"
|
||||
className="h-auto w-full"
|
||||
unoptimized
|
||||
/>
|
||||
<picture>
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt="Image"
|
||||
className="h-auto max-w-full"
|
||||
width="100%"
|
||||
height="auto"
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -93,7 +93,7 @@ export function APIKeyCredentialsModal({
|
||||
<FormDescription>
|
||||
Required scope(s) for this block:{" "}
|
||||
{schema.credentials_scopes?.map((s, i, a) => (
|
||||
<span key={s}>
|
||||
<span key={i}>
|
||||
<code className="text-xs font-bold">{s}</code>
|
||||
{i < a.length - 1 && ", "}
|
||||
</span>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -66,18 +66,16 @@ export function HostScopedCredentialsModal({
|
||||
});
|
||||
|
||||
const [headerPairs, setHeaderPairs] = useState<
|
||||
Array<{ id: string; key: string; value: string }>
|
||||
>([{ id: crypto.randomUUID(), key: "", value: "" }]);
|
||||
Array<{ key: string; value: string }>
|
||||
>([{ key: "", value: "" }]);
|
||||
|
||||
// Update form values when siblingInputs change
|
||||
const prevHostRef = useRef(currentHost);
|
||||
useEffect(() => {
|
||||
if (currentHost === prevHostRef.current) return;
|
||||
prevHostRef.current = currentHost;
|
||||
if (currentHost) {
|
||||
form.setValue("host", currentHost);
|
||||
form.setValue("title", currentHost);
|
||||
} else {
|
||||
// Reset to empty when no current host
|
||||
form.setValue("host", "");
|
||||
form.setValue("title", "Manual Entry");
|
||||
}
|
||||
@@ -93,12 +91,9 @@ export function HostScopedCredentialsModal({
|
||||
|
||||
const { provider, providerName, createHostScopedCredentials } = credentials;
|
||||
|
||||
function addHeaderPair() {
|
||||
setHeaderPairs((prev) => [
|
||||
...prev,
|
||||
{ id: crypto.randomUUID(), key: "", value: "" },
|
||||
]);
|
||||
}
|
||||
const addHeaderPair = () => {
|
||||
setHeaderPairs([...headerPairs, { key: "", value: "" }]);
|
||||
};
|
||||
|
||||
const removeHeaderPair = (index: number) => {
|
||||
if (headerPairs.length > 1) {
|
||||
@@ -197,7 +192,7 @@ export function HostScopedCredentialsModal({
|
||||
</FormDescription>
|
||||
|
||||
{headerPairs.map((pair, index) => (
|
||||
<div key={pair.id} className="flex w-full items-center gap-4">
|
||||
<div key={index} className="flex w-full items-center gap-4">
|
||||
<Input
|
||||
id={`header-${index}-key`}
|
||||
label="Header Name"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Input } from "@/components/__legacy__/ui/input";
|
||||
import { Button } from "@/components/__legacy__/ui/button";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
@@ -7,7 +7,6 @@ import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import { getTimezoneDisplayName } from "@/lib/timezone-utils";
|
||||
import { useUserTimezone } from "@/lib/hooks/useUserTimezone";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
// Base type for cron expression only
|
||||
type CronOnlyCallback = (cronExpression: string) => void;
|
||||
@@ -54,15 +53,15 @@ export function CronSchedulerDialog(props: CronSchedulerDialogProps) {
|
||||
const userTimezone = useUserTimezone();
|
||||
const timezoneDisplay = getTimezoneDisplayName(userTimezone || "UTC");
|
||||
|
||||
// Reset state when dialog opens (render-time sync instead of useEffect)
|
||||
const prevOpenRef = useRef(open);
|
||||
if (open && !prevOpenRef.current) {
|
||||
const defaultName =
|
||||
props.mode === "with-name" ? props.defaultScheduleName || "" : "";
|
||||
setScheduleName(defaultName);
|
||||
setCronExpression(defaultCronExpression);
|
||||
}
|
||||
prevOpenRef.current = open;
|
||||
// Reset state when dialog opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const defaultName =
|
||||
props.mode === "with-name" ? props.defaultScheduleName || "" : "";
|
||||
setScheduleName(defaultName);
|
||||
setCronExpression(defaultCronExpression);
|
||||
}
|
||||
}, [open, props, defaultCronExpression]);
|
||||
|
||||
const handleDone = () => {
|
||||
if (props.mode === "with-name" && !scheduleName.trim()) {
|
||||
@@ -101,11 +100,8 @@ export function CronSchedulerDialog(props: CronSchedulerDialogProps) {
|
||||
<div className="flex flex-col gap-4">
|
||||
{props.mode === "with-name" && (
|
||||
<div className="flex max-w-[448px] flex-col space-y-2">
|
||||
<label htmlFor="schedule-name" className="text-sm font-medium">
|
||||
Schedule Name
|
||||
</label>
|
||||
<label className="text-sm font-medium">Schedule Name</label>
|
||||
<Input
|
||||
id="schedule-name"
|
||||
value={scheduleName}
|
||||
onChange={(e) => setScheduleName(e.target.value)}
|
||||
placeholder="Enter a name for this schedule"
|
||||
@@ -125,9 +121,9 @@ export function CronSchedulerDialog(props: CronSchedulerDialogProps) {
|
||||
<InfoIcon className="h-4 w-4 text-amber-600" />
|
||||
<p className="text-sm text-amber-800">
|
||||
No timezone set. Schedule will run in UTC.
|
||||
<Link href="/profile/settings" className="ml-1 underline">
|
||||
<a href="/profile/settings" className="ml-1 underline">
|
||||
Set your timezone
|
||||
</Link>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -452,7 +452,7 @@ export function CronScheduler({
|
||||
const monthNumber = i + 1;
|
||||
return (
|
||||
<Button
|
||||
key={month.label}
|
||||
key={i}
|
||||
variant={
|
||||
selectedMonths.includes(monthNumber) ? "default" : "outline"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkMath from "remark-math";
|
||||
@@ -360,23 +359,20 @@ function renderMarkdown(
|
||||
</del>
|
||||
),
|
||||
// Image handling
|
||||
img: ({ src, alt }) => {
|
||||
img: ({ src, alt, ...props }) => {
|
||||
// Check if it's a video URL pattern
|
||||
if (src && isVideoUrl(src)) {
|
||||
return renderVideoEmbed(src);
|
||||
}
|
||||
|
||||
if (!src) return null;
|
||||
|
||||
return (
|
||||
<Image
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={src}
|
||||
alt={alt || "Image"}
|
||||
width={0}
|
||||
height={0}
|
||||
sizes="100vw"
|
||||
className="my-4 h-auto w-full rounded-lg shadow-md"
|
||||
unoptimized
|
||||
alt={alt}
|
||||
className="my-4 h-auto max-w-full rounded-lg shadow-md"
|
||||
loading="lazy"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -89,6 +89,7 @@ export function ActivityDropdown({
|
||||
className="!focus:border-1 w-full pr-10"
|
||||
wrapperClassName="!mb-0"
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
onClick={handleClearSearch}
|
||||
|
||||
@@ -218,6 +218,17 @@ If you initially installed Docker with Hyper-V, you **don’t need to reinstall*
|
||||
|
||||
For more details, refer to [Docker's official documentation](https://docs.docker.com/desktop/windows/wsl/).
|
||||
|
||||
### ⚠️ Podman Not Supported
|
||||
|
||||
AutoGPT requires **Docker** (Docker Desktop or Docker Engine). **Podman and podman-compose are not supported** and may cause path resolution issues, particularly on Windows.
|
||||
|
||||
If you see errors like:
|
||||
```text
|
||||
Error: the specified Containerfile or Dockerfile does not exist, ..\..\autogpt_platform\backend\Dockerfile
|
||||
```
|
||||
|
||||
This indicates you're using Podman instead of Docker. Please install [Docker Desktop](https://docs.docker.com/desktop/) and use `docker compose` instead of `podman-compose`.
|
||||
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
Reference in New Issue
Block a user