Compare commits

..

1 Commits

Author SHA1 Message Date
Bently
ef42b17e3b docs: add Podman compatibility warning (#12120)
## Summary
Adds a warning to the Getting Started docs clarifying that **Podman and
podman-compose are not supported**.

## Problem
Users on Windows using `podman-compose` instead of Docker get errors
like:
```
Error: the specified Containerfile or Dockerfile does not exist, ..\..\autogpt_platform\backend\Dockerfile
```

This is because Podman handles relative paths differently than Docker,
causing incorrect path resolution on Windows.

## Solution
- Added a clear warning section after the Windows WSL 2 notes
- Explains the error users might see
- Directs them to install Docker Desktop instead

Closes #11358

<!-- greptile_comment -->

<details><summary><h3>Greptile Summary</h3></summary>

Adds a "Podman Not Supported" warning section to the Getting Started
documentation, placed after the Windows/WSL 2 installation notes. The
section clarifies that Docker is required, shows the typical error
message users encounter when using Podman, and directs them to install
Docker Desktop instead. This addresses issue #11358 where Windows users
using `podman-compose` hit path resolution errors.

- Adds `### ⚠️ Podman Not Supported` section under Manual Setup, after
Windows Installation Note
- Includes the specific error message users see with Podman for easy
identification
- Links to Docker Desktop installation docs as the recommended solution
- Formatting is consistent with existing sections in the document (emoji
headings, code blocks for errors)
</details>


<details><summary><h3>Confidence Score: 5/5</h3></summary>

- This PR is safe to merge — it only adds a documentation warning
section with no code changes.
- The change is a small, well-written documentation addition that adds a
Podman compatibility warning. It touches only one markdown file,
introduces no code changes, and is consistent with the existing document
structure and style. No issues were found.
- No files require special attention.
</details>


<details><summary><h3>Flowchart</h3></summary>

```mermaid
flowchart TD
    A[User wants to run AutoGPT] --> B{Which container runtime?}
    B -->|Docker / Docker Desktop| C[docker compose up -d --build]
    C --> D[AutoGPT starts successfully]
    B -->|Podman / podman-compose| E[podman-compose up -d --build]
    E --> F[Error: Containerfile or Dockerfile does not exist]
    F --> G[New warning section directs user to install Docker Desktop]
    G --> C
```
</details>


<sub>Last reviewed commit: 23ea6bd</sub>

<!-- greptile_other_comments_section -->

<!-- /greptile_comment -->
2026-02-23 15:19:24 +00:00
33 changed files with 384 additions and 533 deletions

View File

@@ -83,65 +83,6 @@ jobs:
- name: Run lint - name: Run lint
run: pnpm 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: chromatic:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: setup needs: setup

View File

@@ -23,8 +23,6 @@
"build-storybook": "storybook build", "build-storybook": "storybook build",
"test-storybook": "test-storybook", "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\"", "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": "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" "generate:api:force": "npx --yes tsx ./scripts/generate-api-queries.ts --force && orval --config ./orval.config.ts"
}, },

View File

@@ -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>
);
}

View File

@@ -7,7 +7,7 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/atoms/Tooltip/BaseTooltip"; } from "@/components/atoms/Tooltip/BaseTooltip";
import { beautifyString, cn } from "@/lib/utils"; import { beautifyString, cn } from "@/lib/utils";
import { useCallback, useState } from "react"; import { useState } from "react";
import { CustomNodeData } from "../CustomNode"; import { CustomNodeData } from "../CustomNode";
import { NodeBadges } from "./NodeBadges"; import { NodeBadges } from "./NodeBadges";
import { NodeContextMenu } from "./NodeContextMenu"; import { NodeContextMenu } from "./NodeContextMenu";
@@ -25,9 +25,6 @@ export const NodeHeader = ({ data, nodeId }: Props) => {
const [isEditingTitle, setIsEditingTitle] = useState(false); const [isEditingTitle, setIsEditingTitle] = useState(false);
const [editedTitle, setEditedTitle] = useState(title); const [editedTitle, setEditedTitle] = useState(title);
const titleInputRef = useCallback((node: HTMLInputElement | null) => {
node?.focus();
}, []);
const handleTitleEdit = () => { const handleTitleEdit = () => {
updateNodeData(nodeId, { updateNodeData(nodeId, {
@@ -55,10 +52,10 @@ export const NodeHeader = ({ data, nodeId }: Props) => {
> >
{isEditingTitle ? ( {isEditingTitle ? (
<input <input
ref={titleInputRef}
id="node-title-input" id="node-title-input"
value={editedTitle} value={editedTitle}
onChange={(e) => setEditedTitle(e.target.value)} onChange={(e) => setEditedTitle(e.target.value)}
autoFocus
className={cn( className={cn(
"m-0 h-fit w-full border-none bg-transparent p-0 focus:outline-none focus:ring-0", "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", "font-sans text-[1rem] font-semibold leading-[1.5rem] text-zinc-800",

View File

@@ -300,6 +300,7 @@ export function MCPToolDialog({
value={serverUrl} value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)} onChange={(e) => setServerUrl(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleDiscoverTools()} onKeyDown={(e) => e.key === "Enter" && handleDiscoverTools()}
autoFocus
/> />
</div> </div>
@@ -326,6 +327,7 @@ export function MCPToolDialog({
value={manualToken} value={manualToken}
onChange={(e) => setManualToken(e.target.value)} onChange={(e) => setManualToken(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleDiscoverTools()} onKeyDown={(e) => e.key === "Enter" && handleDiscoverTools()}
autoFocus
/> />
</div> </div>
)} )}

View File

@@ -52,7 +52,7 @@ export const HorizontalScroll: React.FC<HorizontalScrollAreaProps> = ({
return; return;
} }
const handleScroll = () => updateScrollState(); const handleScroll = () => updateScrollState();
element.addEventListener("scroll", handleScroll, { passive: true }); element.addEventListener("scroll", handleScroll);
window.addEventListener("resize", handleScroll); window.addEventListener("resize", handleScroll);
return () => { return () => {
element.removeEventListener("scroll", handleScroll); element.removeEventListener("scroll", handleScroll);

View File

@@ -85,20 +85,12 @@ export const GraphSearchContent: React.FC<GraphSearchContentProps> = ({
<Tooltip delayDuration={300}> <Tooltip delayDuration={300}>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div <div
role="button"
tabIndex={0}
className={`mx-4 my-2 flex h-20 cursor-pointer rounded-lg border border-zinc-200 bg-white ${ className={`mx-4 my-2 flex h-20 cursor-pointer rounded-lg border border-zinc-200 bg-white ${
index === selectedIndex index === selectedIndex
? "border-zinc-400 shadow-md" ? "border-zinc-400 shadow-md"
: "hover:border-zinc-300 hover:shadow-sm" : "hover:border-zinc-300 hover:shadow-sm"
}`} }`}
onClick={() => onNodeSelect(node.id)} onClick={() => onNodeSelect(node.id)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onNodeSelect(node.id);
}
}}
onMouseEnter={() => { onMouseEnter={() => {
setSelectedIndex(index); setSelectedIndex(index);
onNodeHover?.(node.id); onNodeHover?.(node.id);

View File

@@ -140,7 +140,10 @@ export function AgentRunDraftView({
), ),
[agentInputSchema], [agentInputSchema],
); );
const agentCredentialsInputFields = graph.credentials_input_schema.properties; const agentCredentialsInputFields = useMemo(
() => graph.credentials_input_schema.properties,
[graph],
);
const credentialFields = useMemo( const credentialFields = useMemo(
function getCredentialFields() { function getCredentialFields() {
return Object.entries(agentCredentialsInputFields); return Object.entries(agentCredentialsInputFields);

View File

@@ -1,11 +1,13 @@
import type { Metadata } from "next"; "use client";
import { BuilderContent } from "./BuilderContent"; import { ReactFlowProvider } from "@xyflow/react";
import { Flow } from "./components/FlowEditor/Flow/Flow";
export const metadata: Metadata = {
title: "Build",
description: "Build your agent",
};
export default function BuilderPage() { export default function BuilderPage() {
return <BuilderContent />; return (
<div className="relative h-full w-full">
<ReactFlowProvider>
<Flow />
</ReactFlowProvider>
</div>
);
} }

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { ChatInput } from "@/app/(platform)/copilot/components/ChatInput/ChatInput"; import { ChatInput } from "@/app/(platform)/copilot/components/ChatInput/ChatInput";
import { UIDataTypes, UIMessage, UITools } from "ai"; 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 { ReactNode } from "react";
import { ChatMessagesContainer } from "../ChatMessagesContainer/ChatMessagesContainer"; import { ChatMessagesContainer } from "../ChatMessagesContainer/ChatMessagesContainer";
import { CopilotChatActionsProvider } from "../CopilotChatActionsProvider/CopilotChatActionsProvider"; import { CopilotChatActionsProvider } from "../CopilotChatActionsProvider/CopilotChatActionsProvider";
@@ -38,47 +38,45 @@ export const ChatContainer = ({
const inputLayoutId = "copilot-2-chat-input"; const inputLayoutId = "copilot-2-chat-input";
return ( return (
<LazyMotion features={domAnimation}> <CopilotChatActionsProvider onSend={onSend}>
<CopilotChatActionsProvider onSend={onSend}> <LayoutGroup id="copilot-2-chat-layout">
<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">
<div className="flex h-full min-h-0 w-full flex-col bg-[#f8f8f9] px-2 lg:px-0"> {sessionId ? (
{sessionId ? ( <div className="mx-auto flex h-full min-h-0 w-full max-w-3xl flex-col">
<div className="mx-auto flex h-full min-h-0 w-full max-w-3xl flex-col"> <ChatMessagesContainer
<ChatMessagesContainer messages={messages}
messages={messages} status={status}
status={status} error={error}
error={error} isLoading={isLoadingSession}
isLoading={isLoadingSession} headerSlot={headerSlot}
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}
/> />
)} <motion.div
</div> initial={{ opacity: 0 }}
</LayoutGroup> animate={{ opacity: 1 }}
</CopilotChatActionsProvider> transition={{ duration: 0.3 }}
</LazyMotion> 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>
); );
}; };

View File

@@ -117,7 +117,6 @@ export function AudioWaveform({
{bars.map((height, i) => { {bars.map((height, i) => {
const barHeight = Math.max(minBarHeight, height); const barHeight = Math.max(minBarHeight, height);
return ( return (
// eslint-disable-next-line react/no-array-index-key
<div <div
key={i} key={i}
className="relative" className="relative"

View File

@@ -12,7 +12,6 @@ import {
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner"; import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { toast } from "@/components/molecules/Toast/use-toast"; import { toast } from "@/components/molecules/Toast/use-toast";
import { ToolUIPart, UIDataTypes, UIMessage, UITools } from "ai"; import { ToolUIPart, UIDataTypes, UIMessage, UITools } from "ai";
import Image from "next/image";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { CreateAgentTool } from "../../tools/CreateAgent/CreateAgent"; import { CreateAgentTool } from "../../tools/CreateAgent/CreateAgent";
import { EditAgentTool } from "../../tools/EditAgent/EditAgent"; 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. * Falls back to <video> when an <img> fails to load for workspace files.
*/ */
function WorkspaceMediaImage(props: React.JSX.IntrinsicElements["img"]) { function WorkspaceMediaImage(props: React.JSX.IntrinsicElements["img"]) {
const { src, alt } = props; const { src, alt, ...rest } = props;
const [imgFailed, setImgFailed] = useState(false); const [imgFailed, setImgFailed] = useState(false);
const isWorkspace = src?.includes("/workspace/files/") ?? false; const isWorkspace = src?.includes("/workspace/files/") ?? false;
@@ -80,17 +79,16 @@ function WorkspaceMediaImage(props: React.JSX.IntrinsicElements["img"]) {
} }
return ( return (
<Image // eslint-disable-next-line @next/next/no-img-element
<img
src={src} src={src}
alt={alt || "Image"} alt={alt || "Image"}
width={0} className="h-auto max-w-full rounded-md border border-zinc-200"
height={0} loading="lazy"
sizes="100vw"
className="h-auto w-full rounded-md border border-zinc-200"
unoptimized
onError={() => { onError={() => {
if (isWorkspace) setImgFailed(true); if (isWorkspace) setImgFailed(true);
}} }}
{...rest}
/> />
); );
} }
@@ -197,12 +195,12 @@ export const ChatMessagesContainer = ({
"group-[.is-assistant]:bg-transparent group-[.is-assistant]:text-slate-900" "group-[.is-assistant]:bg-transparent group-[.is-assistant]:text-slate-900"
} }
> >
{message.parts.map((part) => { {message.parts.map((part, i) => {
switch (part.type) { switch (part.type) {
case "text": case "text":
return ( return (
<MessageResponse <MessageResponse
key={`${message.id}-text`} key={`${message.id}-${i}`}
components={STREAMDOWN_COMPONENTS} components={STREAMDOWN_COMPONENTS}
> >
{resolveWorkspaceUrls(part.text)} {resolveWorkspaceUrls(part.text)}
@@ -211,7 +209,7 @@ export const ChatMessagesContainer = ({
case "tool-find_block": case "tool-find_block":
return ( return (
<FindBlocksTool <FindBlocksTool
key={(part as ToolUIPart).toolCallId} key={`${message.id}-${i}`}
part={part as ToolUIPart} part={part as ToolUIPart}
/> />
); );
@@ -219,7 +217,7 @@ export const ChatMessagesContainer = ({
case "tool-find_library_agent": case "tool-find_library_agent":
return ( return (
<FindAgentsTool <FindAgentsTool
key={(part as ToolUIPart).toolCallId} key={`${message.id}-${i}`}
part={part as ToolUIPart} part={part as ToolUIPart}
/> />
); );
@@ -227,14 +225,14 @@ export const ChatMessagesContainer = ({
case "tool-get_doc_page": case "tool-get_doc_page":
return ( return (
<SearchDocsTool <SearchDocsTool
key={(part as ToolUIPart).toolCallId} key={`${message.id}-${i}`}
part={part as ToolUIPart} part={part as ToolUIPart}
/> />
); );
case "tool-run_block": case "tool-run_block":
return ( return (
<RunBlockTool <RunBlockTool
key={(part as ToolUIPart).toolCallId} key={`${message.id}-${i}`}
part={part as ToolUIPart} part={part as ToolUIPart}
/> />
); );
@@ -242,42 +240,42 @@ export const ChatMessagesContainer = ({
case "tool-schedule_agent": case "tool-schedule_agent":
return ( return (
<RunAgentTool <RunAgentTool
key={(part as ToolUIPart).toolCallId} key={`${message.id}-${i}`}
part={part as ToolUIPart} part={part as ToolUIPart}
/> />
); );
case "tool-create_agent": case "tool-create_agent":
return ( return (
<CreateAgentTool <CreateAgentTool
key={(part as ToolUIPart).toolCallId} key={`${message.id}-${i}`}
part={part as ToolUIPart} part={part as ToolUIPart}
/> />
); );
case "tool-edit_agent": case "tool-edit_agent":
return ( return (
<EditAgentTool <EditAgentTool
key={(part as ToolUIPart).toolCallId} key={`${message.id}-${i}`}
part={part as ToolUIPart} part={part as ToolUIPart}
/> />
); );
case "tool-view_agent_output": case "tool-view_agent_output":
return ( return (
<ViewAgentOutputTool <ViewAgentOutputTool
key={(part as ToolUIPart).toolCallId} key={`${message.id}-${i}`}
part={part as ToolUIPart} part={part as ToolUIPart}
/> />
); );
case "tool-search_feature_requests": case "tool-search_feature_requests":
return ( return (
<SearchFeatureRequestsTool <SearchFeatureRequestsTool
key={(part as ToolUIPart).toolCallId} key={`${message.id}-${i}`}
part={part as ToolUIPart} part={part as ToolUIPart}
/> />
); );
case "tool-create_feature_request": case "tool-create_feature_request":
return ( return (
<CreateFeatureRequestTool <CreateFeatureRequestTool
key={(part as ToolUIPart).toolCallId} key={`${message.id}-${i}`}
part={part as ToolUIPart} part={part as ToolUIPart}
/> />
); );
@@ -287,7 +285,7 @@ export const ChatMessagesContainer = ({
if (part.type.startsWith("tool-")) { if (part.type.startsWith("tool-")) {
return ( return (
<GenericTool <GenericTool
key={(part as ToolUIPart).toolCallId} key={`${message.id}-${i}`}
part={part as ToolUIPart} part={part as ToolUIPart}
/> />
); );

View File

@@ -25,7 +25,7 @@ import {
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { DotsThree, PlusCircleIcon, PlusIcon } from "@phosphor-icons/react"; import { DotsThree, PlusCircleIcon, PlusIcon } from "@phosphor-icons/react";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { LazyMotion, domAnimation, m } from "framer-motion"; import { motion } from "framer-motion";
import { parseAsString, useQueryState } from "nuqs"; import { parseAsString, useQueryState } from "nuqs";
import { useState } from "react"; import { useState } from "react";
import { DeleteChatDialog } from "../DeleteChatDialog/DeleteChatDialog"; import { DeleteChatDialog } from "../DeleteChatDialog/DeleteChatDialog";
@@ -129,7 +129,7 @@ export function ChatSidebar() {
} }
return ( return (
<LazyMotion features={domAnimation}> <>
<Sidebar <Sidebar
variant="inset" variant="inset"
collapsible="icon" collapsible="icon"
@@ -144,7 +144,7 @@ export function ChatSidebar() {
: "flex-row items-center justify-between", : "flex-row items-center justify-between",
)} )}
> >
<m.div <motion.div
key={isCollapsed ? "header-collapsed" : "header-expanded"} key={isCollapsed ? "header-collapsed" : "header-expanded"}
className="flex flex-col items-center gap-3 pt-4" className="flex flex-col items-center gap-3 pt-4"
initial={{ opacity: 0, filter: "blur(3px)" }} initial={{ opacity: 0, filter: "blur(3px)" }}
@@ -162,12 +162,12 @@ export function ChatSidebar() {
<span className="sr-only">New Chat</span> <span className="sr-only">New Chat</span>
</Button> </Button>
</div> </div>
</m.div> </motion.div>
</SidebarHeader> </SidebarHeader>
)} )}
<SidebarContent className="gap-4 overflow-y-auto px-4 py-4 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"> <SidebarContent className="gap-4 overflow-y-auto px-4 py-4 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
{!isCollapsed && ( {!isCollapsed && (
<m.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ duration: 0.2, delay: 0.1 }} transition={{ duration: 0.2, delay: 0.1 }}
@@ -179,11 +179,11 @@ export function ChatSidebar() {
<div className="relative left-6"> <div className="relative left-6">
<SidebarTrigger /> <SidebarTrigger />
</div> </div>
</m.div> </motion.div>
)} )}
{!isCollapsed && ( {!isCollapsed && (
<m.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ duration: 0.2, delay: 0.15 }} transition={{ duration: 0.2, delay: 0.15 }}
@@ -256,12 +256,12 @@ export function ChatSidebar() {
</div> </div>
)) ))
)} )}
</m.div> </motion.div>
)} )}
</SidebarContent> </SidebarContent>
{!isCollapsed && sessionId && ( {!isCollapsed && sessionId && (
<SidebarFooter className="shrink-0 bg-zinc-50 p-3 pb-1 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)]"> <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 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ duration: 0.2, delay: 0.2 }} transition={{ duration: 0.2, delay: 0.2 }}
@@ -275,7 +275,7 @@ export function ChatSidebar() {
> >
New Chat New Chat
</Button> </Button>
</m.div> </motion.div>
</SidebarFooter> </SidebarFooter>
)} )}
</Sidebar> </Sidebar>
@@ -286,6 +286,6 @@ export function ChatSidebar() {
onConfirm={handleConfirmDelete} onConfirm={handleConfirmDelete}
onCancel={handleCancelDelete} onCancel={handleCancelDelete}
/> />
</LazyMotion> </>
); );
} }

View File

@@ -5,7 +5,7 @@ import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text"; import { Text } from "@/components/atoms/Text/Text";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { SpinnerGapIcon } from "@phosphor-icons/react"; import { SpinnerGapIcon } from "@phosphor-icons/react";
import { LazyMotion, domAnimation, m } from "framer-motion"; import { motion } from "framer-motion";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import {
getGreetingName, getGreetingName,
@@ -29,7 +29,7 @@ export function EmptySession({
const greetingName = getGreetingName(user); const greetingName = getGreetingName(user);
const quickActions = getQuickActions(); const quickActions = getQuickActions();
const [loadingAction, setLoadingAction] = useState<string | null>(null); const [loadingAction, setLoadingAction] = useState<string | null>(null);
const [inputPlaceholder, setInputPlaceholder] = useState(() => const [inputPlaceholder, setInputPlaceholder] = useState(
getInputPlaceholder(), getInputPlaceholder(),
); );
@@ -49,65 +49,63 @@ export function EmptySession({
} }
return ( 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">
<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
<m.div className="w-full max-w-3xl text-center"
className="w-full max-w-3xl text-center" initial={{ opacity: 0 }}
initial={{ opacity: 0 }} animate={{ opacity: 1 }}
animate={{ opacity: 1 }} transition={{ duration: 0.3 }}
transition={{ duration: 0.3 }} >
> <div className="mx-auto max-w-3xl">
<div className="mx-auto max-w-3xl"> <Text variant="h3" className="mb-1 !text-[1.375rem] text-zinc-700">
<Text variant="h3" className="mb-1 !text-[1.375rem] text-zinc-700"> Hey, <span className="text-violet-600">{greetingName}</span>
Hey, <span className="text-violet-600">{greetingName}</span> </Text>
</Text> <Text variant="h3" className="mb-8 !font-normal">
<Text variant="h3" className="mb-8 !font-normal"> Tell me about your work I&apos;ll find what to automate.
Tell me about your work I&apos;ll find what to automate. </Text>
</Text>
<div className="mb-6"> <div className="mb-6">
<m.div <motion.div
layoutId={inputLayoutId} layoutId={inputLayoutId}
transition={{ type: "spring", bounce: 0.2, duration: 0.65 }} transition={{ type: "spring", bounce: 0.2, duration: 0.65 }}
className="w-full px-2" className="w-full px-2"
> >
<ChatInput <ChatInput
inputId="chat-input-empty" inputId="chat-input-empty"
onSend={onSend} onSend={onSend}
disabled={isCreatingSession} disabled={isCreatingSession}
placeholder={inputPlaceholder} placeholder={inputPlaceholder}
className="w-full" className="w-full"
/> />
</m.div> </motion.div>
</div>
</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"> <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) => ( {quickActions.map((action) => (
<Button <Button
key={action} key={action}
type="button" type="button"
variant="outline" variant="outline"
size="small" size="small"
onClick={() => void handleQuickActionClick(action)} onClick={() => void handleQuickActionClick(action)}
disabled={isCreatingSession || loadingAction !== null} disabled={isCreatingSession || loadingAction !== null}
aria-busy={loadingAction === action} aria-busy={loadingAction === action}
leftIcon={ leftIcon={
loadingAction === action ? ( loadingAction === action ? (
<SpinnerGapIcon <SpinnerGapIcon
className="h-4 w-4 animate-spin" className="h-4 w-4 animate-spin"
weight="bold" weight="bold"
/> />
) : null ) : null
} }
className="h-auto shrink-0 border-zinc-300 px-3 py-2 text-[.9rem] text-zinc-600" className="h-auto shrink-0 border-zinc-300 px-3 py-2 text-[.9rem] text-zinc-600"
> >
{action} {action}
</Button> </Button>
))} ))}
</div> </div>
</m.div> </motion.div>
</div> </div>
</LazyMotion>
); );
} }

View File

@@ -1,5 +1,5 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { AnimatePresence, LazyMotion, domAnimation, m } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
interface Props { interface Props {
text: string; text: string;
@@ -10,47 +10,45 @@ export function MorphingTextAnimation({ text, className }: Props) {
const letters = text.split(""); const letters = text.split("");
return ( return (
<LazyMotion features={domAnimation}> <div className={cn(className)}>
<div className={cn(className)}> <AnimatePresence mode="popLayout" initial={false}>
<AnimatePresence mode="popLayout" initial={false}> <motion.div key={text} className="whitespace-nowrap">
<m.div key={text} className="whitespace-nowrap"> <motion.span className="inline-flex overflow-hidden">
<m.span className="inline-flex overflow-hidden"> {letters.map((char, index) => (
{letters.map((char, index) => ( <motion.span
// eslint-disable-next-line react/no-array-index-key key={`${text}-${index}`}
<m.span initial={{
key={`${text}-${index}`} opacity: 0,
initial={{ y: 8,
opacity: 0, rotateX: "80deg",
y: 8, filter: "blur(6px)",
rotateX: "80deg", }}
filter: "blur(6px)", animate={{
}} opacity: 1,
animate={{ y: 0,
opacity: 1, rotateX: "0deg",
y: 0, filter: "blur(0px)",
rotateX: "0deg", }}
filter: "blur(0px)", exit={{
}} opacity: 0,
exit={{ y: -8,
opacity: 0, rotateX: "-80deg",
y: -8, filter: "blur(6px)",
rotateX: "-80deg", }}
filter: "blur(6px)", style={{ willChange: "transform" }}
}} transition={{
transition={{ delay: 0.015 * index,
delay: 0.015 * index, type: "spring",
type: "spring", bounce: 0.5,
bounce: 0.5, }}
}} className="inline-block"
className="inline-block" >
> {char === " " ? "\u00A0" : char}
{char === " " ? "\u00A0" : char} </motion.span>
</m.span> ))}
))} </motion.span>
</m.span> </motion.div>
</m.div> </AnimatePresence>
</AnimatePresence> </div>
</div>
</LazyMotion>
); );
} }

View File

@@ -2,13 +2,7 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { CaretDownIcon } from "@phosphor-icons/react"; import { CaretDownIcon } from "@phosphor-icons/react";
import { import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
AnimatePresence,
LazyMotion,
domAnimation,
m,
useReducedMotion,
} from "framer-motion";
import { useId } from "react"; import { useId } from "react";
import { useToolAccordion } from "./useToolAccordion"; import { useToolAccordion } from "./useToolAccordion";
@@ -44,66 +38,65 @@ export function ToolAccordion({
}); });
return ( return (
<LazyMotion features={domAnimation}> <div
<div className={cn(
className={cn( "mt-2 w-full rounded-lg border border-slate-200 bg-slate-100 px-3 py-2",
"mt-2 w-full rounded-lg border border-slate-200 bg-slate-100 px-3 py-2", className,
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 <div className="flex min-w-0 items-center gap-3">
type="button" <span className="flex shrink-0 items-center text-gray-800">
aria-expanded={isExpanded} {icon}
aria-controls={contentId} </span>
onClick={toggle} <div className="min-w-0">
className="flex w-full items-center justify-between gap-3 py-1 text-left" <p
> className={cn(
<div className="flex min-w-0 items-center gap-3"> "truncate text-sm font-medium text-gray-800",
<span className="flex shrink-0 items-center text-gray-800"> titleClassName,
{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>
</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> {title}
</m.div> </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> weight="bold"
</div> />
</LazyMotion> </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>
); );
} }

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -161,7 +161,7 @@ export function ClarificationQuestionsCard({
return ( return (
<div <div
key={q.keyword} key={`${q.keyword}-${index}`}
className={cn( className={cn(
"relative rounded-lg border p-3", "relative rounded-lg border p-3",
isAnswered isAnswered

View File

@@ -557,11 +557,8 @@ function getTodoAccordionData(input: unknown): AccordionData {
description: `${completed}/${total} completed`, description: `${completed}/${total} completed`,
content: ( content: (
<div className="space-y-1 py-1"> <div className="space-y-1 py-1">
{todos.map((todo, idx) => ( {todos.map((todo, i) => (
<div <div key={i} className="flex items-start gap-2 text-xs">
key={`${todo.status}:${todo.content}:${idx}`}
className="flex items-start gap-2 text-xs"
>
<span className="mt-0.5 flex-shrink-0"> <span className="mt-0.5 flex-shrink-0">
{todo.status === "completed" ? ( {todo.status === "completed" ? (
<CheckCircleIcon <CheckCircleIcon

View File

@@ -4,7 +4,7 @@ import type { AgentDetailsResponse } from "@/app/api/__generated__/models/agentD
import { Button } from "@/components/atoms/Button/Button"; import { Button } from "@/components/atoms/Button/Button";
import { Text } from "@/components/atoms/Text/Text"; import { Text } from "@/components/atoms/Text/Text";
import { FormRenderer } from "@/components/renderers/InputRenderer/FormRenderer"; 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 { useState } from "react";
import { useCopilotChatActions } from "../../../../components/CopilotChatActionsProvider/useCopilotChatActions"; import { useCopilotChatActions } from "../../../../components/CopilotChatActionsProvider/useCopilotChatActions";
import { ContentMessage } from "../../../../components/ToolAccordion/AccordionContent"; import { ContentMessage } from "../../../../components/ToolAccordion/AccordionContent";
@@ -39,83 +39,78 @@ export function AgentDetailsCard({ output }: Props) {
} }
return ( return (
<LazyMotion features={domAnimation}> <div className="grid gap-2">
<div className="grid gap-2"> <ContentMessage>
<ContentMessage> Run this agent with example values or your own inputs.
Run this agent with example values or your own inputs. </ContentMessage>
</ContentMessage>
<div className="flex gap-2 pt-4"> <div className="flex gap-2 pt-4">
<Button <Button size="small" className="w-fit" onClick={handleRunWithExamples}>
size="small" Run with example values
className="w-fit" </Button>
onClick={handleRunWithExamples} <Button
> variant="outline"
Run with example values size="small"
</Button> className="w-fit"
<Button onClick={() => setShowInputForm((prev) => !prev)}
variant="outline" >
size="small" Run with my inputs
className="w-fit" </Button>
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> </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>
); );
} }

View File

@@ -103,7 +103,7 @@ function OutputKeySection({
</div> </div>
<div className="mt-2"> <div className="mt-2">
{visibleItems.map((item, i) => ( {visibleItems.map((item, i) => (
<RenderOutputValue key={`${outputKey}-${i}`} value={item} /> <RenderOutputValue key={i} value={item} />
))} ))}
</div> </div>
{hasMoreItems && ( {hasMoreItems && (

View File

@@ -209,10 +209,7 @@ export function ViewAgentOutputTool({ part }: Props) {
</div> </div>
<div className="mt-2"> <div className="mt-2">
{items.slice(0, 3).map((item, i) => ( {items.slice(0, 3).map((item, i) => (
<RenderOutputValue <RenderOutputValue key={i} value={item} />
key={`${key}-${i}`}
value={item}
/>
))} ))}
</div> </div>
</ContentCard> </ContentCard>

View File

@@ -23,23 +23,13 @@ export function SidebarItemCard({
onClick, onClick,
actions, actions,
}: Props) { }: Props) {
function handleKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onClick?.();
}
}
return ( return (
<div <div
role="button"
tabIndex={0}
className={cn( 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", "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, selected ? "border-slate-800 ring-slate-800" : undefined,
)} )}
onClick={onClick} onClick={onClick}
onKeyDown={handleKeyDown}
> >
<div className="flex min-w-0 items-center justify-start gap-3"> <div className="flex min-w-0 items-center justify-start gap-3">
{icon} {icon}
@@ -59,13 +49,7 @@ export function SidebarItemCard({
</Text> </Text>
</div> </div>
{actions ? ( {actions ? (
<div <div onClick={(e) => e.stopPropagation()}>{actions}</div>
role="presentation"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
{actions}
</div>
) : null} ) : null}
</div> </div>
</div> </div>

View File

@@ -1,12 +1,15 @@
import { redirect } from "next/navigation"; "use client";
import type { Metadata } from "next"; import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { useRouter } from "next/navigation";
export const metadata: Metadata = { import { useEffect } from "react";
title: "AutoGPT Platform",
description: "AutoGPT Platform",
};
export default function Page() { export default function Page() {
redirect("/copilot"); const router = useRouter();
useEffect(() => {
router.replace("/copilot");
}, [router]);
return <LoadingSpinner size="large" cover />;
} }

View File

@@ -1,7 +1,6 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import Image from "next/image";
const getYouTubeVideoId = (url: string) => { const getYouTubeVideoId = (url: string) => {
const regExp = const regExp =
@@ -77,7 +76,6 @@ const VideoRenderer: React.FC<{ videoUrl: string }> = ({ videoUrl }) => {
width="100%" width="100%"
height="315" height="315"
src={`https://www.youtube.com/embed/${videoId}`} src={`https://www.youtube.com/embed/${videoId}`}
title="Embedded content"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen allowFullScreen
></iframe> ></iframe>
@@ -94,15 +92,15 @@ const VideoRenderer: React.FC<{ videoUrl: string }> = ({ videoUrl }) => {
const ImageRenderer: React.FC<{ imageUrl: string }> = ({ imageUrl }) => { const ImageRenderer: React.FC<{ imageUrl: string }> = ({ imageUrl }) => {
return ( return (
<div className="w-full p-2"> <div className="w-full p-2">
<Image <picture>
src={imageUrl} <img
alt="Image" src={imageUrl}
width={0} alt="Image"
height={0} className="h-auto max-w-full"
sizes="100vw" width="100%"
className="h-auto w-full" height="auto"
unoptimized />
/> </picture>
</div> </div>
); );
}; };

View File

@@ -93,7 +93,7 @@ export function APIKeyCredentialsModal({
<FormDescription> <FormDescription>
Required scope(s) for this block:{" "} Required scope(s) for this block:{" "}
{schema.credentials_scopes?.map((s, i, a) => ( {schema.credentials_scopes?.map((s, i, a) => (
<span key={s}> <span key={i}>
<code className="text-xs font-bold">{s}</code> <code className="text-xs font-bold">{s}</code>
{i < a.length - 1 && ", "} {i < a.length - 1 && ", "}
</span> </span>

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useState } from "react";
import { z } from "zod"; import { z } from "zod";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@@ -66,18 +66,16 @@ export function HostScopedCredentialsModal({
}); });
const [headerPairs, setHeaderPairs] = useState< const [headerPairs, setHeaderPairs] = useState<
Array<{ id: string; key: string; value: string }> Array<{ key: string; value: string }>
>([{ id: crypto.randomUUID(), key: "", value: "" }]); >([{ key: "", value: "" }]);
// Update form values when siblingInputs change // Update form values when siblingInputs change
const prevHostRef = useRef(currentHost);
useEffect(() => { useEffect(() => {
if (currentHost === prevHostRef.current) return;
prevHostRef.current = currentHost;
if (currentHost) { if (currentHost) {
form.setValue("host", currentHost); form.setValue("host", currentHost);
form.setValue("title", currentHost); form.setValue("title", currentHost);
} else { } else {
// Reset to empty when no current host
form.setValue("host", ""); form.setValue("host", "");
form.setValue("title", "Manual Entry"); form.setValue("title", "Manual Entry");
} }
@@ -93,12 +91,9 @@ export function HostScopedCredentialsModal({
const { provider, providerName, createHostScopedCredentials } = credentials; const { provider, providerName, createHostScopedCredentials } = credentials;
function addHeaderPair() { const addHeaderPair = () => {
setHeaderPairs((prev) => [ setHeaderPairs([...headerPairs, { key: "", value: "" }]);
...prev, };
{ id: crypto.randomUUID(), key: "", value: "" },
]);
}
const removeHeaderPair = (index: number) => { const removeHeaderPair = (index: number) => {
if (headerPairs.length > 1) { if (headerPairs.length > 1) {
@@ -197,7 +192,7 @@ export function HostScopedCredentialsModal({
</FormDescription> </FormDescription>
{headerPairs.map((pair, index) => ( {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 <Input
id={`header-${index}-key`} id={`header-${index}-key`}
label="Header Name" label="Header Name"

View File

@@ -1,4 +1,4 @@
import { useRef, useState } from "react"; import { useEffect, useState } from "react";
import { Input } from "@/components/__legacy__/ui/input"; import { Input } from "@/components/__legacy__/ui/input";
import { Button } from "@/components/__legacy__/ui/button"; import { Button } from "@/components/__legacy__/ui/button";
import { useToast } from "@/components/molecules/Toast/use-toast"; 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 { getTimezoneDisplayName } from "@/lib/timezone-utils";
import { useUserTimezone } from "@/lib/hooks/useUserTimezone"; import { useUserTimezone } from "@/lib/hooks/useUserTimezone";
import { InfoIcon } from "lucide-react"; import { InfoIcon } from "lucide-react";
import Link from "next/link";
// Base type for cron expression only // Base type for cron expression only
type CronOnlyCallback = (cronExpression: string) => void; type CronOnlyCallback = (cronExpression: string) => void;
@@ -54,15 +53,15 @@ export function CronSchedulerDialog(props: CronSchedulerDialogProps) {
const userTimezone = useUserTimezone(); const userTimezone = useUserTimezone();
const timezoneDisplay = getTimezoneDisplayName(userTimezone || "UTC"); const timezoneDisplay = getTimezoneDisplayName(userTimezone || "UTC");
// Reset state when dialog opens (render-time sync instead of useEffect) // Reset state when dialog opens
const prevOpenRef = useRef(open); useEffect(() => {
if (open && !prevOpenRef.current) { if (open) {
const defaultName = const defaultName =
props.mode === "with-name" ? props.defaultScheduleName || "" : ""; props.mode === "with-name" ? props.defaultScheduleName || "" : "";
setScheduleName(defaultName); setScheduleName(defaultName);
setCronExpression(defaultCronExpression); setCronExpression(defaultCronExpression);
} }
prevOpenRef.current = open; }, [open, props, defaultCronExpression]);
const handleDone = () => { const handleDone = () => {
if (props.mode === "with-name" && !scheduleName.trim()) { if (props.mode === "with-name" && !scheduleName.trim()) {
@@ -101,11 +100,8 @@ export function CronSchedulerDialog(props: CronSchedulerDialogProps) {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{props.mode === "with-name" && ( {props.mode === "with-name" && (
<div className="flex max-w-[448px] flex-col space-y-2"> <div className="flex max-w-[448px] flex-col space-y-2">
<label htmlFor="schedule-name" className="text-sm font-medium"> <label className="text-sm font-medium">Schedule Name</label>
Schedule Name
</label>
<Input <Input
id="schedule-name"
value={scheduleName} value={scheduleName}
onChange={(e) => setScheduleName(e.target.value)} onChange={(e) => setScheduleName(e.target.value)}
placeholder="Enter a name for this schedule" 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" /> <InfoIcon className="h-4 w-4 text-amber-600" />
<p className="text-sm text-amber-800"> <p className="text-sm text-amber-800">
No timezone set. Schedule will run in UTC. 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 Set your timezone
</Link> </a>
</p> </p>
</div> </div>
) : ( ) : (

View File

@@ -452,7 +452,7 @@ export function CronScheduler({
const monthNumber = i + 1; const monthNumber = i + 1;
return ( return (
<Button <Button
key={month.label} key={i}
variant={ variant={
selectedMonths.includes(monthNumber) ? "default" : "outline" selectedMonths.includes(monthNumber) ? "default" : "outline"
} }

View File

@@ -1,7 +1,6 @@
"use client"; "use client";
import React from "react"; import React from "react";
import Image from "next/image";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import remarkMath from "remark-math"; import remarkMath from "remark-math";
@@ -360,23 +359,20 @@ function renderMarkdown(
</del> </del>
), ),
// Image handling // Image handling
img: ({ src, alt }) => { img: ({ src, alt, ...props }) => {
// Check if it's a video URL pattern // Check if it's a video URL pattern
if (src && isVideoUrl(src)) { if (src && isVideoUrl(src)) {
return renderVideoEmbed(src); return renderVideoEmbed(src);
} }
if (!src) return null;
return ( return (
<Image // eslint-disable-next-line @next/next/no-img-element
<img
src={src} src={src}
alt={alt || "Image"} alt={alt}
width={0} className="my-4 h-auto max-w-full rounded-lg shadow-md"
height={0} loading="lazy"
sizes="100vw" {...props}
className="my-4 h-auto w-full rounded-lg shadow-md"
unoptimized
/> />
); );
}, },

View File

@@ -89,6 +89,7 @@ export function ActivityDropdown({
className="!focus:border-1 w-full pr-10" className="!focus:border-1 w-full pr-10"
wrapperClassName="!mb-0" wrapperClassName="!mb-0"
autoComplete="off" autoComplete="off"
autoFocus
/> />
<button <button
onClick={handleClearSearch} onClick={handleClearSearch}

View File

@@ -218,6 +218,17 @@ If you initially installed Docker with Hyper-V, you **dont need to reinstall*
For more details, refer to [Docker's official documentation](https://docs.docker.com/desktop/windows/wsl/). 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 ## Development