Compare commits

..

2 Commits

Author SHA1 Message Date
Lluis Agusti
0e62f43652 chore: wip 2026-01-22 23:47:02 +07:00
Lluis Agusti
49e0fb5f40 chore: improve loading states 2026-01-22 23:12:50 +07:00
176 changed files with 1060 additions and 978 deletions

View File

@@ -34,10 +34,7 @@ logger = logging.getLogger(__name__)
# Default output directory relative to repo root # Default output directory relative to repo root
DEFAULT_OUTPUT_DIR = ( DEFAULT_OUTPUT_DIR = (
Path(__file__).parent.parent.parent.parent Path(__file__).parent.parent.parent.parent / "docs" / "integrations"
/ "docs"
/ "integrations"
/ "block-integrations"
) )
@@ -424,14 +421,6 @@ def generate_block_markdown(
lines.append("<!-- END MANUAL -->") lines.append("<!-- END MANUAL -->")
lines.append("") lines.append("")
# Optional per-block extras (only include if has content)
extras = manual_content.get("extras", "")
if extras:
lines.append("<!-- MANUAL: extras -->")
lines.append(extras)
lines.append("<!-- END MANUAL -->")
lines.append("")
lines.append("---") lines.append("---")
lines.append("") lines.append("")
@@ -467,52 +456,25 @@ def get_block_file_mapping(blocks: list[BlockDoc]) -> dict[str, list[BlockDoc]]:
return dict(file_mapping) return dict(file_mapping)
def generate_overview_table(blocks: list[BlockDoc], block_dir_prefix: str = "") -> str: def generate_overview_table(blocks: list[BlockDoc]) -> str:
"""Generate the overview table markdown (blocks.md). """Generate the overview table markdown (blocks.md)."""
Args:
blocks: List of block documentation objects
block_dir_prefix: Prefix for block file links (e.g., "block-integrations/")
"""
lines = [] lines = []
# GitBook YAML frontmatter
lines.append("---")
lines.append("layout:")
lines.append(" width: default")
lines.append(" title:")
lines.append(" visible: true")
lines.append(" description:")
lines.append(" visible: true")
lines.append(" tableOfContents:")
lines.append(" visible: false")
lines.append(" outline:")
lines.append(" visible: true")
lines.append(" pagination:")
lines.append(" visible: true")
lines.append(" metadata:")
lines.append(" visible: true")
lines.append("---")
lines.append("")
lines.append("# AutoGPT Blocks Overview") lines.append("# AutoGPT Blocks Overview")
lines.append("") lines.append("")
lines.append( lines.append(
'AutoGPT uses a modular approach with various "blocks" to handle different tasks. These blocks are the building blocks of AutoGPT workflows, allowing users to create complex automations by combining simple, specialized components.' 'AutoGPT uses a modular approach with various "blocks" to handle different tasks. These blocks are the building blocks of AutoGPT workflows, allowing users to create complex automations by combining simple, specialized components.'
) )
lines.append("") lines.append("")
lines.append('{% hint style="info" %}') lines.append('!!! info "Creating Your Own Blocks"')
lines.append("**Creating Your Own Blocks**") lines.append(" Want to create your own custom blocks? Check out our guides:")
lines.append("") lines.append(" ")
lines.append("Want to create your own custom blocks? Check out our guides:")
lines.append("")
lines.append( lines.append(
"* [Build your own Blocks](https://docs.agpt.co/platform/new_blocks/) - Step-by-step tutorial with examples" " - [Build your own Blocks](https://docs.agpt.co/platform/new_blocks/) - Step-by-step tutorial with examples"
) )
lines.append( lines.append(
"* [Block SDK Guide](https://docs.agpt.co/platform/block-sdk-guide/) - Advanced SDK patterns with OAuth, webhooks, and provider configuration" " - [Block SDK Guide](https://docs.agpt.co/platform/block-sdk-guide/) - Advanced SDK patterns with OAuth, webhooks, and provider configuration"
) )
lines.append("{% endhint %}")
lines.append("") lines.append("")
lines.append( lines.append(
"Below is a comprehensive list of all available blocks, categorized by their primary function. Click on any block name to view its detailed documentation." "Below is a comprehensive list of all available blocks, categorized by their primary function. Click on any block name to view its detailed documentation."
@@ -575,8 +537,7 @@ def generate_overview_table(blocks: list[BlockDoc], block_dir_prefix: str = "")
else "No description" else "No description"
) )
short_desc = short_desc.replace("\n", " ").replace("|", "\\|") short_desc = short_desc.replace("\n", " ").replace("|", "\\|")
link_path = f"{block_dir_prefix}{file_path}" lines.append(f"| [{block.name}]({file_path}#{anchor}) | {short_desc} |")
lines.append(f"| [{block.name}]({link_path}#{anchor}) | {short_desc} |")
lines.append("") lines.append("")
continue continue
@@ -602,55 +563,13 @@ def generate_overview_table(blocks: list[BlockDoc], block_dir_prefix: str = "")
) )
short_desc = short_desc.replace("\n", " ").replace("|", "\\|") short_desc = short_desc.replace("\n", " ").replace("|", "\\|")
link_path = f"{block_dir_prefix}{file_path}" lines.append(f"| [{block.name}]({file_path}#{anchor}) | {short_desc} |")
lines.append(f"| [{block.name}]({link_path}#{anchor}) | {short_desc} |")
lines.append("") lines.append("")
return "\n".join(lines) return "\n".join(lines)
def generate_summary_md(
blocks: list[BlockDoc], root_dir: Path, block_dir_prefix: str = ""
) -> str:
"""Generate SUMMARY.md for GitBook navigation.
Args:
blocks: List of block documentation objects
root_dir: The root docs directory (e.g., docs/integrations/)
block_dir_prefix: Prefix for block file links (e.g., "block-integrations/")
"""
lines = []
lines.append("# Table of contents")
lines.append("")
lines.append("* [AutoGPT Blocks Overview](README.md)")
lines.append("")
# Check for guides/ directory at the root level (docs/integrations/guides/)
guides_dir = root_dir / "guides"
if guides_dir.exists():
lines.append("## Guides")
lines.append("")
for guide_file in sorted(guides_dir.glob("*.md")):
# Use just the file name for title (replace hyphens/underscores with spaces)
title = file_path_to_title(guide_file.stem.replace("-", "_") + ".md")
lines.append(f"* [{title}](guides/{guide_file.name})")
lines.append("")
lines.append("## Block Integrations")
lines.append("")
file_mapping = get_block_file_mapping(blocks)
for file_path in sorted(file_mapping.keys()):
title = file_path_to_title(file_path)
link_path = f"{block_dir_prefix}{file_path}"
lines.append(f"* [{title}]({link_path})")
lines.append("")
return "\n".join(lines)
def load_all_blocks_for_docs() -> list[BlockDoc]: def load_all_blocks_for_docs() -> list[BlockDoc]:
"""Load all blocks and extract documentation.""" """Load all blocks and extract documentation."""
from backend.blocks import load_all_blocks from backend.blocks import load_all_blocks
@@ -734,16 +653,6 @@ def write_block_docs(
) )
) )
# Add file-level additional_content section if present
file_additional = extract_manual_content(existing_content).get(
"additional_content", ""
)
if file_additional:
content_parts.append("<!-- MANUAL: additional_content -->")
content_parts.append(file_additional)
content_parts.append("<!-- END MANUAL -->")
content_parts.append("")
full_content = file_header + "\n" + "\n".join(content_parts) full_content = file_header + "\n" + "\n".join(content_parts)
generated_files[str(file_path)] = full_content generated_files[str(file_path)] = full_content
@@ -752,28 +661,14 @@ def write_block_docs(
full_path.write_text(full_content) full_path.write_text(full_content)
# Generate overview file at the parent directory (docs/integrations/) # Generate overview file
# with links prefixed to point into block-integrations/ overview_content = generate_overview_table(blocks)
root_dir = output_dir.parent overview_path = output_dir / "README.md"
block_dir_name = output_dir.name # "block-integrations"
block_dir_prefix = f"{block_dir_name}/"
overview_content = generate_overview_table(blocks, block_dir_prefix)
overview_path = root_dir / "README.md"
generated_files["README.md"] = overview_content generated_files["README.md"] = overview_content
overview_path.write_text(overview_content) overview_path.write_text(overview_content)
if verbose: if verbose:
print(" Writing README.md (overview) to parent directory") print(" Writing README.md (overview)")
# Generate SUMMARY.md for GitBook navigation at the parent directory
summary_content = generate_summary_md(blocks, root_dir, block_dir_prefix)
summary_path = root_dir / "SUMMARY.md"
generated_files["SUMMARY.md"] = summary_content
summary_path.write_text(summary_content)
if verbose:
print(" Writing SUMMARY.md (navigation) to parent directory")
return generated_files return generated_files
@@ -853,16 +748,6 @@ def check_docs_in_sync(output_dir: Path, blocks: list[BlockDoc]) -> bool:
elif block_match.group(1).strip() != expected_block_content.strip(): elif block_match.group(1).strip() != expected_block_content.strip():
mismatched_blocks.append(block.name) mismatched_blocks.append(block.name)
# Add file-level additional_content to expected content (matches write_block_docs)
file_additional = extract_manual_content(existing_content).get(
"additional_content", ""
)
if file_additional:
content_parts.append("<!-- MANUAL: additional_content -->")
content_parts.append(file_additional)
content_parts.append("<!-- END MANUAL -->")
content_parts.append("")
expected_content = file_header + "\n" + "\n".join(content_parts) expected_content = file_header + "\n" + "\n".join(content_parts)
if existing_content.strip() != expected_content.strip(): if existing_content.strip() != expected_content.strip():
@@ -872,15 +757,11 @@ def check_docs_in_sync(output_dir: Path, blocks: list[BlockDoc]) -> bool:
out_of_sync_details.append((file_path, mismatched_blocks)) out_of_sync_details.append((file_path, mismatched_blocks))
all_match = False all_match = False
# Check overview at the parent directory (docs/integrations/) # Check overview
root_dir = output_dir.parent overview_path = output_dir / "README.md"
block_dir_name = output_dir.name # "block-integrations"
block_dir_prefix = f"{block_dir_name}/"
overview_path = root_dir / "README.md"
if overview_path.exists(): if overview_path.exists():
existing_overview = overview_path.read_text() existing_overview = overview_path.read_text()
expected_overview = generate_overview_table(blocks, block_dir_prefix) expected_overview = generate_overview_table(blocks)
if existing_overview.strip() != expected_overview.strip(): if existing_overview.strip() != expected_overview.strip():
print("OUT OF SYNC: README.md (overview)") print("OUT OF SYNC: README.md (overview)")
print(" The blocks overview table needs regeneration") print(" The blocks overview table needs regeneration")
@@ -891,21 +772,6 @@ def check_docs_in_sync(output_dir: Path, blocks: list[BlockDoc]) -> bool:
out_of_sync_details.append(("README.md", ["overview table"])) out_of_sync_details.append(("README.md", ["overview table"]))
all_match = False all_match = False
# Check SUMMARY.md at the parent directory
summary_path = root_dir / "SUMMARY.md"
if summary_path.exists():
existing_summary = summary_path.read_text()
expected_summary = generate_summary_md(blocks, root_dir, block_dir_prefix)
if existing_summary.strip() != expected_summary.strip():
print("OUT OF SYNC: SUMMARY.md (navigation)")
print(" The GitBook navigation needs regeneration")
out_of_sync_details.append(("SUMMARY.md", ["navigation"]))
all_match = False
else:
print("MISSING: SUMMARY.md (navigation)")
out_of_sync_details.append(("SUMMARY.md", ["navigation"]))
all_match = False
# Check for unfilled manual sections # Check for unfilled manual sections
unfilled_patterns = [ unfilled_patterns = [
"_Add a description of this category of blocks._", "_Add a description of this category of blocks._",

View File

@@ -0,0 +1,41 @@
"use client";
import { createContext, useContext, useRef, type ReactNode } from "react";
interface NewChatContextValue {
onNewChatClick: () => void;
setOnNewChatClick: (handler?: () => void) => void;
performNewChat?: () => void;
setPerformNewChat: (handler?: () => void) => void;
}
const NewChatContext = createContext<NewChatContextValue | null>(null);
export function NewChatProvider({ children }: { children: ReactNode }) {
const onNewChatRef = useRef<(() => void) | undefined>();
const performNewChatRef = useRef<(() => void) | undefined>();
const contextValueRef = useRef<NewChatContextValue>({
onNewChatClick() {
onNewChatRef.current?.();
},
setOnNewChatClick(handler?: () => void) {
onNewChatRef.current = handler;
},
performNewChat() {
performNewChatRef.current?.();
},
setPerformNewChat(handler?: () => void) {
performNewChatRef.current = handler;
},
});
return (
<NewChatContext.Provider value={contextValueRef.current}>
{children}
</NewChatContext.Provider>
);
}
export function useNewChat() {
return useContext(NewChatContext);
}

View File

@@ -1,8 +1,10 @@
"use client"; "use client";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner"; import { ChatLoader } from "@/components/contextual/Chat/components/ChatLoader/ChatLoader";
import { NAVBAR_HEIGHT_PX } from "@/lib/constants"; import { NAVBAR_HEIGHT_PX } from "@/lib/constants";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { useEffect } from "react";
import { useNewChat } from "../../NewChatContext";
import { DesktopSidebar } from "./components/DesktopSidebar/DesktopSidebar"; import { DesktopSidebar } from "./components/DesktopSidebar/DesktopSidebar";
import { LoadingState } from "./components/LoadingState/LoadingState"; import { LoadingState } from "./components/LoadingState/LoadingState";
import { MobileDrawer } from "./components/MobileDrawer/MobileDrawer"; import { MobileDrawer } from "./components/MobileDrawer/MobileDrawer";
@@ -33,10 +35,25 @@ export function CopilotShell({ children }: Props) {
isReadyToShowContent, isReadyToShowContent,
} = useCopilotShell(); } = useCopilotShell();
const newChatContext = useNewChat();
const handleNewChatClickWrapper =
newChatContext?.onNewChatClick || handleNewChat;
useEffect(
function registerNewChatHandler() {
if (!newChatContext) return;
newChatContext.setPerformNewChat(handleNewChat);
return function cleanup() {
newChatContext.setPerformNewChat(undefined);
};
},
[newChatContext, handleNewChat],
);
if (!isLoggedIn) { if (!isLoggedIn) {
return ( return (
<div className="flex h-full items-center justify-center"> <div className="flex h-full items-center justify-center">
<LoadingSpinner size="large" /> <ChatLoader />
</div> </div>
); );
} }
@@ -55,7 +72,7 @@ export function CopilotShell({ children }: Props) {
isFetchingNextPage={isFetchingNextPage} isFetchingNextPage={isFetchingNextPage}
onSelectSession={handleSelectSession} onSelectSession={handleSelectSession}
onFetchNextPage={fetchNextPage} onFetchNextPage={fetchNextPage}
onNewChat={handleNewChat} onNewChat={handleNewChatClickWrapper}
hasActiveSession={Boolean(hasActiveSession)} hasActiveSession={Boolean(hasActiveSession)}
/> />
)} )}
@@ -77,7 +94,7 @@ export function CopilotShell({ children }: Props) {
isFetchingNextPage={isFetchingNextPage} isFetchingNextPage={isFetchingNextPage}
onSelectSession={handleSelectSession} onSelectSession={handleSelectSession}
onFetchNextPage={fetchNextPage} onFetchNextPage={fetchNextPage}
onNewChat={handleNewChat} onNewChat={handleNewChatClickWrapper}
onClose={handleCloseDrawer} onClose={handleCloseDrawer}
onOpenChange={handleDrawerOpenChange} onOpenChange={handleDrawerOpenChange}
hasActiveSession={Boolean(hasActiveSession)} hasActiveSession={Boolean(hasActiveSession)}

View File

@@ -115,13 +115,13 @@ export function useCopilotShell() {
const isReadyToShowContent = isOnHomepage const isReadyToShowContent = isOnHomepage
? true ? true
: checkReadyToShowContent( : checkReadyToShowContent(
areAllSessionsLoaded, areAllSessionsLoaded,
paramSessionId, paramSessionId,
accumulatedSessions, accumulatedSessions,
isCurrentSessionLoading, isCurrentSessionLoading,
currentSessionData, currentSessionData,
hasAutoSelectedSession, hasAutoSelectedSession,
); );
function handleSelectSession(sessionId: string) { function handleSelectSession(sessionId: string) {
// Navigate using replaceState to avoid full page reload // Navigate using replaceState to avoid full page reload
@@ -148,13 +148,15 @@ export function useCopilotShell() {
setHasAutoSelectedSession(false); setHasAutoSelectedSession(false);
} }
const isLoading = isSessionsLoading && accumulatedSessions.length === 0;
return { return {
isMobile, isMobile,
isDrawerOpen, isDrawerOpen,
isLoggedIn, isLoggedIn,
hasActiveSession: hasActiveSession:
Boolean(currentSessionId) && (!isOnHomepage || Boolean(paramSessionId)), Boolean(currentSessionId) && (!isOnHomepage || Boolean(paramSessionId)),
isLoading: isSessionsLoading || !areAllSessionsLoaded, isLoading,
sessions: visibleSessions, sessions: visibleSessions,
currentSessionId: sidebarSelectedSessionId, currentSessionId: sidebarSelectedSessionId,
handleSelectSession, handleSelectSession,

View File

@@ -1,5 +1,28 @@
import type { User } from "@supabase/supabase-js"; import type { User } from "@supabase/supabase-js";
export type PageState =
| { type: "welcome" }
| { type: "newChat" }
| { type: "creating"; prompt: string }
| { type: "chat"; sessionId: string; initialPrompt?: string };
export function getInitialPromptFromState(
pageState: PageState,
storedInitialPrompt: string | undefined,
) {
if (storedInitialPrompt) return storedInitialPrompt;
if (pageState.type === "creating") return pageState.prompt;
if (pageState.type === "chat") return pageState.initialPrompt;
}
export function shouldResetToWelcome(pageState: PageState) {
return (
pageState.type !== "newChat" &&
pageState.type !== "creating" &&
pageState.type !== "welcome"
);
}
export function getGreetingName(user?: User | null): string { export function getGreetingName(user?: User | null): string {
if (!user) return "there"; if (!user) return "there";
const metadata = user.user_metadata as Record<string, unknown> | undefined; const metadata = user.user_metadata as Record<string, unknown> | undefined;

View File

@@ -1,6 +1,11 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { NewChatProvider } from "./NewChatContext";
import { CopilotShell } from "./components/CopilotShell/CopilotShell"; import { CopilotShell } from "./components/CopilotShell/CopilotShell";
export default function CopilotLayout({ children }: { children: ReactNode }) { export default function CopilotLayout({ children }: { children: ReactNode }) {
return <CopilotShell>{children}</CopilotShell>; return (
<NewChatProvider>
<CopilotShell>{children}</CopilotShell>
</NewChatProvider>
);
} }

View File

@@ -1,142 +1,35 @@
"use client"; "use client";
import { postV2CreateSession } from "@/app/api/__generated__/endpoints/chat/chat";
import { Skeleton } from "@/components/__legacy__/ui/skeleton"; import { Skeleton } from "@/components/__legacy__/ui/skeleton";
import { Button } from "@/components/atoms/Button/Button"; import { Button } from "@/components/atoms/Button/Button";
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
import { Text } from "@/components/atoms/Text/Text"; import { Text } from "@/components/atoms/Text/Text";
import { Chat } from "@/components/contextual/Chat/Chat"; import { Chat } from "@/components/contextual/Chat/Chat";
import { ChatInput } from "@/components/contextual/Chat/components/ChatInput/ChatInput"; import { ChatInput } from "@/components/contextual/Chat/components/ChatInput/ChatInput";
import { getHomepageRoute } from "@/lib/constants"; import { ChatLoader } from "@/components/contextual/Chat/components/ChatLoader/ChatLoader";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { import { useCopilotPage } from "./useCopilotPage";
Flag,
type FlagValues,
useGetFlag,
} from "@/services/feature-flags/use-get-flag";
import { useFlags } from "launchdarkly-react-client-sdk";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useMemo, useRef, useState } from "react";
import { getGreetingName, getQuickActions } from "./helpers";
type PageState =
| { type: "welcome" }
| { type: "creating"; prompt: string }
| { type: "chat"; sessionId: string; initialPrompt?: string };
export default function CopilotPage() { export default function CopilotPage() {
const router = useRouter(); const { state, handlers } = useCopilotPage();
const searchParams = useSearchParams(); const {
const { user, isLoggedIn, isUserLoading } = useSupabase(); greetingName,
quickActions,
isLoading,
pageState,
isNewChatModalOpen,
isReady,
} = state;
const {
handleQuickAction,
startChatWithPrompt,
handleSessionNotFound,
handleStreamingChange,
handleCancelNewChat,
proceedWithNewChat,
handleNewChatModalOpen,
} = handlers;
const isChatEnabled = useGetFlag(Flag.CHAT); if (!isReady) {
const flags = useFlags<FlagValues>();
const homepageRoute = getHomepageRoute(isChatEnabled);
const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true";
const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID;
const isLaunchDarklyConfigured = envEnabled && Boolean(clientId);
const isFlagReady =
!isLaunchDarklyConfigured || flags[Flag.CHAT] !== undefined;
const [pageState, setPageState] = useState<PageState>({ type: "welcome" });
const initialPromptRef = useRef<Map<string, string>>(new Map());
const urlSessionId = searchParams.get("sessionId");
// Sync with URL sessionId (preserve initialPrompt from ref)
useEffect(
function syncSessionFromUrl() {
if (urlSessionId) {
// If we're already in chat state with this sessionId, don't overwrite
if (pageState.type === "chat" && pageState.sessionId === urlSessionId) {
return;
}
// Get initialPrompt from ref or current state
const storedInitialPrompt = initialPromptRef.current.get(urlSessionId);
const currentInitialPrompt =
storedInitialPrompt ||
(pageState.type === "creating"
? pageState.prompt
: pageState.type === "chat"
? pageState.initialPrompt
: undefined);
if (currentInitialPrompt) {
initialPromptRef.current.set(urlSessionId, currentInitialPrompt);
}
setPageState({
type: "chat",
sessionId: urlSessionId,
initialPrompt: currentInitialPrompt,
});
} else if (pageState.type === "chat") {
setPageState({ type: "welcome" });
}
},
[urlSessionId],
);
useEffect(
function ensureAccess() {
if (!isFlagReady) return;
if (isChatEnabled === false) {
router.replace(homepageRoute);
}
},
[homepageRoute, isChatEnabled, isFlagReady, router],
);
const greetingName = useMemo(
function getName() {
return getGreetingName(user);
},
[user],
);
const quickActions = useMemo(function getActions() {
return getQuickActions();
}, []);
async function startChatWithPrompt(prompt: string) {
if (!prompt?.trim()) return;
if (pageState.type === "creating") return;
const trimmedPrompt = prompt.trim();
setPageState({ type: "creating", prompt: trimmedPrompt });
try {
// Create session
const sessionResponse = await postV2CreateSession({
body: JSON.stringify({}),
});
if (sessionResponse.status !== 200 || !sessionResponse.data?.id) {
throw new Error("Failed to create session");
}
const sessionId = sessionResponse.data.id;
// Store initialPrompt in ref so it persists across re-renders
initialPromptRef.current.set(sessionId, trimmedPrompt);
// Update URL and show Chat with initial prompt
// Chat will handle sending the message and streaming
window.history.replaceState(null, "", `/copilot?sessionId=${sessionId}`);
setPageState({ type: "chat", sessionId, initialPrompt: trimmedPrompt });
} catch (error) {
console.error("[CopilotPage] Failed to start chat:", error);
setPageState({ type: "welcome" });
}
}
function handleQuickAction(action: string) {
startChatWithPrompt(action);
}
function handleSessionNotFound() {
router.replace("/copilot");
}
if (!isFlagReady || isChatEnabled === false || !isLoggedIn) {
return null; return null;
} }
@@ -150,7 +43,55 @@ export default function CopilotPage() {
urlSessionId={pageState.sessionId} urlSessionId={pageState.sessionId}
initialPrompt={pageState.initialPrompt} initialPrompt={pageState.initialPrompt}
onSessionNotFound={handleSessionNotFound} onSessionNotFound={handleSessionNotFound}
onStreamingChange={handleStreamingChange}
/> />
<Dialog
title="Interrupt current chat?"
styling={{ maxWidth: 300, width: "100%" }}
controlled={{
isOpen: isNewChatModalOpen,
set: handleNewChatModalOpen,
}}
onClose={handleCancelNewChat}
>
<Dialog.Content>
<div className="flex flex-col gap-4">
<Text variant="body">
The current chat response will be interrupted. Are you sure you
want to start a new chat?
</Text>
<Dialog.Footer>
<Button
type="button"
variant="outline"
onClick={handleCancelNewChat}
>
Cancel
</Button>
<Button
type="button"
variant="primary"
onClick={proceedWithNewChat}
>
Start new chat
</Button>
</Dialog.Footer>
</div>
</Dialog.Content>
</Dialog>
</div>
);
}
if (pageState.type === "newChat") {
return (
<div className="flex h-full flex-1 flex-col items-center justify-center bg-[#f8f8f9]">
<div className="flex flex-col items-center gap-4">
<ChatLoader />
<Text variant="body" className="text-zinc-500">
Loading your chats...
</Text>
</div>
</div> </div>
); );
} }
@@ -158,18 +99,18 @@ export default function CopilotPage() {
// Show loading state while creating session and sending first message // Show loading state while creating session and sending first message
if (pageState.type === "creating") { if (pageState.type === "creating") {
return ( return (
<div className="flex h-full flex-1 flex-col items-center justify-center bg-[#f8f8f9] px-6 py-10"> <div className="flex h-full flex-1 flex-col items-center justify-center bg-[#f8f8f9]">
<LoadingSpinner size="large" /> <div className="flex flex-col items-center gap-4">
<Text variant="body" className="mt-4 text-zinc-500"> <ChatLoader />
Starting your chat... <Text variant="body" className="text-zinc-500">
</Text> Loading your chats...
</Text>
</div>
</div> </div>
); );
} }
// Show Welcome screen // Show Welcome screen
const isLoading = isUserLoading;
return ( return (
<div className="flex h-full flex-1 items-center justify-center overflow-y-auto bg-[#f8f8f9] px-6 py-10"> <div className="flex h-full flex-1 items-center justify-center overflow-y-auto bg-[#f8f8f9] px-6 py-10">
<div className="w-full text-center"> <div className="w-full text-center">

View File

@@ -0,0 +1,258 @@
import { postV2CreateSession } from "@/app/api/__generated__/endpoints/chat/chat";
import { getHomepageRoute } from "@/lib/constants";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import {
Flag,
type FlagValues,
useGetFlag,
} from "@/services/feature-flags/use-get-flag";
import { useFlags } from "launchdarkly-react-client-sdk";
import { useRouter } from "next/navigation";
import { useEffect, useReducer } from "react";
import { useNewChat } from "./NewChatContext";
import {
getGreetingName,
getQuickActions,
type PageState,
} from "./helpers";
import { useCopilotURLState } from "./useCopilotURLState";
type CopilotState = {
pageState: PageState;
isStreaming: boolean;
isNewChatModalOpen: boolean;
initialPrompts: Record<string, string>;
previousSessionId: string | null;
};
type CopilotAction =
| { type: "setPageState"; pageState: PageState }
| { type: "setStreaming"; isStreaming: boolean }
| { type: "setNewChatModalOpen"; isOpen: boolean }
| { type: "setInitialPrompt"; sessionId: string; prompt: string }
| { type: "setPreviousSessionId"; sessionId: string | null };
function isSamePageState(next: PageState, current: PageState) {
if (next.type !== current.type) return false;
if (next.type === "creating" && current.type === "creating") {
return next.prompt === current.prompt;
}
if (next.type === "chat" && current.type === "chat") {
return (
next.sessionId === current.sessionId &&
next.initialPrompt === current.initialPrompt
);
}
return true;
}
function copilotReducer(state: CopilotState, action: CopilotAction): CopilotState {
if (action.type === "setPageState") {
if (isSamePageState(action.pageState, state.pageState)) return state;
return { ...state, pageState: action.pageState };
}
if (action.type === "setStreaming") {
if (action.isStreaming === state.isStreaming) return state;
return { ...state, isStreaming: action.isStreaming };
}
if (action.type === "setNewChatModalOpen") {
if (action.isOpen === state.isNewChatModalOpen) return state;
return { ...state, isNewChatModalOpen: action.isOpen };
}
if (action.type === "setInitialPrompt") {
if (state.initialPrompts[action.sessionId] === action.prompt) return state;
return {
...state,
initialPrompts: {
...state.initialPrompts,
[action.sessionId]: action.prompt,
},
};
}
if (action.type === "setPreviousSessionId") {
if (state.previousSessionId === action.sessionId) return state;
return { ...state, previousSessionId: action.sessionId };
}
return state;
}
export function useCopilotPage() {
const router = useRouter();
const { user, isLoggedIn, isUserLoading } = useSupabase();
const isChatEnabled = useGetFlag(Flag.CHAT);
const flags = useFlags<FlagValues>();
const homepageRoute = getHomepageRoute(isChatEnabled);
const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true";
const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID;
const isLaunchDarklyConfigured = envEnabled && Boolean(clientId);
const isFlagReady =
!isLaunchDarklyConfigured || flags[Flag.CHAT] !== undefined;
const [state, dispatch] = useReducer(copilotReducer, {
pageState: { type: "welcome" },
isStreaming: false,
isNewChatModalOpen: false,
initialPrompts: {},
previousSessionId: null,
});
const newChatContext = useNewChat();
const greetingName = getGreetingName(user);
const quickActions = getQuickActions();
function setPageState(pageState: PageState) {
dispatch({ type: "setPageState", pageState });
}
function setInitialPrompt(sessionId: string, prompt: string) {
dispatch({ type: "setInitialPrompt", sessionId, prompt });
}
function setPreviousSessionId(sessionId: string | null) {
dispatch({ type: "setPreviousSessionId", sessionId });
}
const { setUrlSessionId } = useCopilotURLState({
pageState: state.pageState,
initialPrompts: state.initialPrompts,
previousSessionId: state.previousSessionId,
setPageState,
setInitialPrompt,
setPreviousSessionId,
});
useEffect(
function registerNewChatHandler() {
if (!newChatContext) return;
newChatContext.setOnNewChatClick(handleNewChatClick);
return function cleanup() {
newChatContext.setOnNewChatClick(undefined);
};
},
[newChatContext, handleNewChatClick],
);
useEffect(
function transitionNewChatToWelcome() {
if (state.pageState.type === "newChat") {
function setWelcomeState() {
dispatch({ type: "setPageState", pageState: { type: "welcome" } });
}
const timer = setTimeout(setWelcomeState, 300);
return function cleanup() {
clearTimeout(timer);
};
}
},
[state.pageState.type],
);
useEffect(
function ensureAccess() {
if (!isFlagReady) return;
if (isChatEnabled === false) {
router.replace(homepageRoute);
}
},
[homepageRoute, isChatEnabled, isFlagReady, router],
);
async function startChatWithPrompt(prompt: string) {
if (!prompt?.trim()) return;
if (state.pageState.type === "creating") return;
const trimmedPrompt = prompt.trim();
dispatch({
type: "setPageState",
pageState: { type: "creating", prompt: trimmedPrompt },
});
try {
const sessionResponse = await postV2CreateSession({
body: JSON.stringify({}),
});
if (sessionResponse.status !== 200 || !sessionResponse.data?.id) {
throw new Error("Failed to create session");
}
const sessionId = sessionResponse.data.id;
dispatch({
type: "setInitialPrompt",
sessionId,
prompt: trimmedPrompt,
});
await setUrlSessionId(sessionId, { shallow: false });
dispatch({
type: "setPageState",
pageState: { type: "chat", sessionId, initialPrompt: trimmedPrompt },
});
} catch (error) {
console.error("[CopilotPage] Failed to start chat:", error);
dispatch({ type: "setPageState", pageState: { type: "welcome" } });
}
}
function handleQuickAction(action: string) {
startChatWithPrompt(action);
}
function handleSessionNotFound() {
router.replace("/copilot");
}
function handleStreamingChange(isStreamingValue: boolean) {
dispatch({ type: "setStreaming", isStreaming: isStreamingValue });
}
function proceedWithNewChat() {
dispatch({ type: "setNewChatModalOpen", isOpen: false });
if (newChatContext?.performNewChat) {
newChatContext.performNewChat();
return;
}
setUrlSessionId(null, { shallow: false });
router.replace("/copilot");
}
function handleCancelNewChat() {
dispatch({ type: "setNewChatModalOpen", isOpen: false });
}
function handleNewChatModalOpen(isOpen: boolean) {
dispatch({ type: "setNewChatModalOpen", isOpen });
}
function handleNewChatClick() {
if (state.isStreaming) {
dispatch({ type: "setNewChatModalOpen", isOpen: true });
} else {
proceedWithNewChat();
}
}
return {
state: {
greetingName,
quickActions,
isLoading: isUserLoading,
pageState: state.pageState,
isNewChatModalOpen: state.isNewChatModalOpen,
isReady: isFlagReady && isChatEnabled !== false && isLoggedIn,
},
handlers: {
handleQuickAction,
startChatWithPrompt,
handleSessionNotFound,
handleStreamingChange,
handleCancelNewChat,
proceedWithNewChat,
handleNewChatModalOpen,
},
};
}

View File

@@ -0,0 +1,80 @@
import { parseAsString, useQueryState } from "nuqs";
import { useLayoutEffect } from "react";
import {
getInitialPromptFromState,
type PageState,
shouldResetToWelcome,
} from "./helpers";
interface UseCopilotUrlStateArgs {
pageState: PageState;
initialPrompts: Record<string, string>;
previousSessionId: string | null;
setPageState: (pageState: PageState) => void;
setInitialPrompt: (sessionId: string, prompt: string) => void;
setPreviousSessionId: (sessionId: string | null) => void;
}
export function useCopilotURLState({
pageState,
initialPrompts,
previousSessionId,
setPageState,
setInitialPrompt,
setPreviousSessionId,
}: UseCopilotUrlStateArgs) {
const [urlSessionId, setUrlSessionId] = useQueryState(
"sessionId",
parseAsString,
);
function syncSessionFromUrl() {
if (urlSessionId) {
if (pageState.type === "chat" && pageState.sessionId === urlSessionId) {
setPreviousSessionId(urlSessionId);
return;
}
const storedInitialPrompt = initialPrompts[urlSessionId];
const currentInitialPrompt = getInitialPromptFromState(
pageState,
storedInitialPrompt,
);
if (currentInitialPrompt) {
setInitialPrompt(urlSessionId, currentInitialPrompt);
}
setPageState({
type: "chat",
sessionId: urlSessionId,
initialPrompt: currentInitialPrompt,
});
setPreviousSessionId(urlSessionId);
return;
}
const wasInChat = previousSessionId !== null && pageState.type === "chat";
setPreviousSessionId(null);
if (wasInChat) {
setPageState({ type: "newChat" });
return;
}
if (shouldResetToWelcome(pageState)) {
setPageState({ type: "welcome" });
}
}
useLayoutEffect(syncSessionFromUrl, [
urlSessionId,
pageState.type,
previousSessionId,
initialPrompts,
]);
return {
urlSessionId,
setUrlSessionId,
};
}

View File

@@ -13,6 +13,7 @@ export interface ChatProps {
urlSessionId?: string | null; urlSessionId?: string | null;
initialPrompt?: string; initialPrompt?: string;
onSessionNotFound?: () => void; onSessionNotFound?: () => void;
onStreamingChange?: (isStreaming: boolean) => void;
} }
export function Chat({ export function Chat({
@@ -20,6 +21,7 @@ export function Chat({
urlSessionId, urlSessionId,
initialPrompt, initialPrompt,
onSessionNotFound, onSessionNotFound,
onStreamingChange,
}: ChatProps) { }: ChatProps) {
const hasHandledNotFoundRef = useRef(false); const hasHandledNotFoundRef = useRef(false);
const { const {
@@ -73,6 +75,7 @@ export function Chat({
initialMessages={messages} initialMessages={messages}
initialPrompt={initialPrompt} initialPrompt={initialPrompt}
className="flex-1" className="flex-1"
onStreamingChange={onStreamingChange}
/> />
)} )}
</main> </main>

View File

@@ -4,6 +4,7 @@ import { Text } from "@/components/atoms/Text/Text";
import { Dialog } from "@/components/molecules/Dialog/Dialog"; import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { useBreakpoint } from "@/lib/hooks/useBreakpoint"; import { useBreakpoint } from "@/lib/hooks/useBreakpoint";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useEffect } from "react";
import { ChatInput } from "../ChatInput/ChatInput"; import { ChatInput } from "../ChatInput/ChatInput";
import { MessageList } from "../MessageList/MessageList"; import { MessageList } from "../MessageList/MessageList";
import { useChatContainer } from "./useChatContainer"; import { useChatContainer } from "./useChatContainer";
@@ -13,6 +14,7 @@ export interface ChatContainerProps {
initialMessages: SessionDetailResponse["messages"]; initialMessages: SessionDetailResponse["messages"];
initialPrompt?: string; initialPrompt?: string;
className?: string; className?: string;
onStreamingChange?: (isStreaming: boolean) => void;
} }
export function ChatContainer({ export function ChatContainer({
@@ -20,6 +22,7 @@ export function ChatContainer({
initialMessages, initialMessages,
initialPrompt, initialPrompt,
className, className,
onStreamingChange,
}: ChatContainerProps) { }: ChatContainerProps) {
const { const {
messages, messages,
@@ -36,6 +39,10 @@ export function ChatContainer({
initialPrompt, initialPrompt,
}); });
useEffect(() => {
onStreamingChange?.(isStreaming);
}, [isStreaming, onStreamingChange]);
const breakpoint = useBreakpoint(); const breakpoint = useBreakpoint();
const isMobile = const isMobile =
breakpoint === "base" || breakpoint === "sm" || breakpoint === "md"; breakpoint === "base" || breakpoint === "sm" || breakpoint === "md";

View File

@@ -1,12 +1,8 @@
import { Text } from "@/components/atoms/Text/Text";
export function ChatLoader() { export function ChatLoader() {
return ( return (
<Text <div className="flex items-center gap-2">
variant="small" <div className="h-5 w-5 rounded-full bg-black animate-loader" />
className="bg-gradient-to-r from-neutral-600 via-neutral-500 to-neutral-600 bg-[length:200%_100%] bg-clip-text text-xs text-transparent [animation:shimmer_2s_ease-in-out_infinite]" </div>
>
Taking a bit more time...
</Text>
); );
} }

View File

@@ -7,7 +7,6 @@ import {
ArrowsClockwiseIcon, ArrowsClockwiseIcon,
CheckCircleIcon, CheckCircleIcon,
CheckIcon, CheckIcon,
CopyIcon,
} from "@phosphor-icons/react"; } from "@phosphor-icons/react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
@@ -340,11 +339,26 @@ export function ChatMessage({
size="icon" size="icon"
onClick={handleCopy} onClick={handleCopy}
aria-label="Copy message" aria-label="Copy message"
className="p-1"
> >
{copied ? ( {copied ? (
<CheckIcon className="size-4 text-green-600" /> <CheckIcon className="size-4 text-green-600" />
) : ( ) : (
<CopyIcon className="size-4 text-zinc-600" /> <svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="size-3 text-zinc-600"
>
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
</svg>
)} )}
</Button> </Button>
)} )}

View File

@@ -1,7 +1,6 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { AIChatBubble } from "../AIChatBubble/AIChatBubble"; import { AIChatBubble } from "../AIChatBubble/AIChatBubble";
import { ChatLoader } from "../ChatLoader/ChatLoader";
export interface ThinkingMessageProps { export interface ThinkingMessageProps {
className?: string; className?: string;
@@ -9,7 +8,9 @@ export interface ThinkingMessageProps {
export function ThinkingMessage({ className }: ThinkingMessageProps) { export function ThinkingMessage({ className }: ThinkingMessageProps) {
const [showSlowLoader, setShowSlowLoader] = useState(false); const [showSlowLoader, setShowSlowLoader] = useState(false);
const [showCoffeeMessage, setShowCoffeeMessage] = useState(false);
const timerRef = useRef<NodeJS.Timeout | null>(null); const timerRef = useRef<NodeJS.Timeout | null>(null);
const coffeeTimerRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => { useEffect(() => {
if (timerRef.current === null) { if (timerRef.current === null) {
@@ -18,11 +19,21 @@ export function ThinkingMessage({ className }: ThinkingMessageProps) {
}, 8000); }, 8000);
} }
if (coffeeTimerRef.current === null) {
coffeeTimerRef.current = setTimeout(() => {
setShowCoffeeMessage(true);
}, 10000);
}
return () => { return () => {
if (timerRef.current) { if (timerRef.current) {
clearTimeout(timerRef.current); clearTimeout(timerRef.current);
timerRef.current = null; timerRef.current = null;
} }
if (coffeeTimerRef.current) {
clearTimeout(coffeeTimerRef.current);
coffeeTimerRef.current = null;
}
}; };
}, []); }, []);
@@ -37,16 +48,16 @@ export function ThinkingMessage({ className }: ThinkingMessageProps) {
<div className="flex min-w-0 flex-1 flex-col"> <div className="flex min-w-0 flex-1 flex-col">
<AIChatBubble> <AIChatBubble>
<div className="transition-all duration-500 ease-in-out"> <div className="transition-all duration-500 ease-in-out">
{showSlowLoader ? ( {showCoffeeMessage ? (
<ChatLoader /> <span className="inline-block bg-gradient-to-r from-neutral-400 via-neutral-600 to-neutral-400 bg-[length:200%_100%] bg-clip-text text-transparent animate-shimmer">
This could take a few minutes, grab a coffee
</span>
) : showSlowLoader ? (
<span className="inline-block bg-gradient-to-r from-neutral-400 via-neutral-600 to-neutral-400 bg-[length:200%_100%] bg-clip-text text-transparent animate-shimmer">
Taking a bit more time...
</span>
) : ( ) : (
<span <span className="inline-block bg-gradient-to-r from-neutral-400 via-neutral-600 to-neutral-400 bg-[length:200%_100%] bg-clip-text text-transparent animate-shimmer">
className="inline-block bg-gradient-to-r from-neutral-400 via-neutral-600 to-neutral-400 bg-clip-text text-transparent"
style={{
backgroundSize: "200% 100%",
animation: "shimmer 2s ease-in-out infinite",
}}
>
Thinking... Thinking...
</span> </span>
)} )}

View File

@@ -157,12 +157,21 @@ const config = {
backgroundPosition: "-200% 0", backgroundPosition: "-200% 0",
}, },
}, },
loader: {
"0%": {
boxShadow: "0 0 0 0 rgba(0, 0, 0, 0.25)",
},
"100%": {
boxShadow: "0 0 0 30px rgba(0, 0, 0, 0)",
},
},
}, },
animation: { animation: {
"accordion-down": "accordion-down 0.2s ease-out", "accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out",
"fade-in": "fade-in 0.2s ease-out", "fade-in": "fade-in 0.2s ease-out",
shimmer: "shimmer 2s ease-in-out infinite", shimmer: "shimmer 2s ease-in-out infinite",
loader: "loader 1s infinite",
}, },
transitionDuration: { transitionDuration: {
"2000": "2000ms", "2000": "2000ms",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 504 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -1,133 +0,0 @@
# Table of contents
* [AutoGPT Blocks Overview](README.md)
## Guides
* [LLM Providers](guides/llm-providers.md)
* [Voice Providers](guides/voice-providers.md)
## Block Integrations
* [Airtable Bases](block-integrations/airtable/bases.md)
* [Airtable Records](block-integrations/airtable/records.md)
* [Airtable Schema](block-integrations/airtable/schema.md)
* [Airtable Triggers](block-integrations/airtable/triggers.md)
* [Apollo Organization](block-integrations/apollo/organization.md)
* [Apollo People](block-integrations/apollo/people.md)
* [Apollo Person](block-integrations/apollo/person.md)
* [Ayrshare Post To Bluesky](block-integrations/ayrshare/post_to_bluesky.md)
* [Ayrshare Post To Facebook](block-integrations/ayrshare/post_to_facebook.md)
* [Ayrshare Post To GMB](block-integrations/ayrshare/post_to_gmb.md)
* [Ayrshare Post To Instagram](block-integrations/ayrshare/post_to_instagram.md)
* [Ayrshare Post To LinkedIn](block-integrations/ayrshare/post_to_linkedin.md)
* [Ayrshare Post To Pinterest](block-integrations/ayrshare/post_to_pinterest.md)
* [Ayrshare Post To Reddit](block-integrations/ayrshare/post_to_reddit.md)
* [Ayrshare Post To Snapchat](block-integrations/ayrshare/post_to_snapchat.md)
* [Ayrshare Post To Telegram](block-integrations/ayrshare/post_to_telegram.md)
* [Ayrshare Post To Threads](block-integrations/ayrshare/post_to_threads.md)
* [Ayrshare Post To TikTok](block-integrations/ayrshare/post_to_tiktok.md)
* [Ayrshare Post To X](block-integrations/ayrshare/post_to_x.md)
* [Ayrshare Post To YouTube](block-integrations/ayrshare/post_to_youtube.md)
* [Baas Bots](block-integrations/baas/bots.md)
* [Bannerbear Text Overlay](block-integrations/bannerbear/text_overlay.md)
* [Basic](block-integrations/basic.md)
* [Compass Triggers](block-integrations/compass/triggers.md)
* [Data](block-integrations/data.md)
* [Dataforseo Keyword Suggestions](block-integrations/dataforseo/keyword_suggestions.md)
* [Dataforseo Related Keywords](block-integrations/dataforseo/related_keywords.md)
* [Discord Bot Blocks](block-integrations/discord/bot_blocks.md)
* [Discord OAuth Blocks](block-integrations/discord/oauth_blocks.md)
* [Enrichlayer LinkedIn](block-integrations/enrichlayer/linkedin.md)
* [Exa Answers](block-integrations/exa/answers.md)
* [Exa Code Context](block-integrations/exa/code_context.md)
* [Exa Contents](block-integrations/exa/contents.md)
* [Exa Research](block-integrations/exa/research.md)
* [Exa Search](block-integrations/exa/search.md)
* [Exa Similar](block-integrations/exa/similar.md)
* [Exa Webhook Blocks](block-integrations/exa/webhook_blocks.md)
* [Exa Websets](block-integrations/exa/websets.md)
* [Exa Websets Enrichment](block-integrations/exa/websets_enrichment.md)
* [Exa Websets Import Export](block-integrations/exa/websets_import_export.md)
* [Exa Websets Items](block-integrations/exa/websets_items.md)
* [Exa Websets Monitor](block-integrations/exa/websets_monitor.md)
* [Exa Websets Polling](block-integrations/exa/websets_polling.md)
* [Exa Websets Search](block-integrations/exa/websets_search.md)
* [Fal AI Video Generator](block-integrations/fal/ai_video_generator.md)
* [Firecrawl Crawl](block-integrations/firecrawl/crawl.md)
* [Firecrawl Extract](block-integrations/firecrawl/extract.md)
* [Firecrawl Map](block-integrations/firecrawl/map.md)
* [Firecrawl Scrape](block-integrations/firecrawl/scrape.md)
* [Firecrawl Search](block-integrations/firecrawl/search.md)
* [Generic Webhook Triggers](block-integrations/generic_webhook/triggers.md)
* [GitHub Checks](block-integrations/github/checks.md)
* [GitHub CI](block-integrations/github/ci.md)
* [GitHub Issues](block-integrations/github/issues.md)
* [GitHub Pull Requests](block-integrations/github/pull_requests.md)
* [GitHub Repo](block-integrations/github/repo.md)
* [GitHub Reviews](block-integrations/github/reviews.md)
* [GitHub Statuses](block-integrations/github/statuses.md)
* [GitHub Triggers](block-integrations/github/triggers.md)
* [Google Calendar](block-integrations/google/calendar.md)
* [Google Docs](block-integrations/google/docs.md)
* [Google Gmail](block-integrations/google/gmail.md)
* [Google Sheets](block-integrations/google/sheets.md)
* [HubSpot Company](block-integrations/hubspot/company.md)
* [HubSpot Contact](block-integrations/hubspot/contact.md)
* [HubSpot Engagement](block-integrations/hubspot/engagement.md)
* [Jina Chunking](block-integrations/jina/chunking.md)
* [Jina Embeddings](block-integrations/jina/embeddings.md)
* [Jina Fact Checker](block-integrations/jina/fact_checker.md)
* [Jina Search](block-integrations/jina/search.md)
* [Linear Comment](block-integrations/linear/comment.md)
* [Linear Issues](block-integrations/linear/issues.md)
* [Linear Projects](block-integrations/linear/projects.md)
* [LLM](block-integrations/llm.md)
* [Logic](block-integrations/logic.md)
* [Misc](block-integrations/misc.md)
* [Multimedia](block-integrations/multimedia.md)
* [Notion Create Page](block-integrations/notion/create_page.md)
* [Notion Read Database](block-integrations/notion/read_database.md)
* [Notion Read Page](block-integrations/notion/read_page.md)
* [Notion Read Page Markdown](block-integrations/notion/read_page_markdown.md)
* [Notion Search](block-integrations/notion/search.md)
* [Nvidia Deepfake](block-integrations/nvidia/deepfake.md)
* [Replicate Flux Advanced](block-integrations/replicate/flux_advanced.md)
* [Replicate Replicate Block](block-integrations/replicate/replicate_block.md)
* [Search](block-integrations/search.md)
* [Slant3D Filament](block-integrations/slant3d/filament.md)
* [Slant3D Order](block-integrations/slant3d/order.md)
* [Slant3D Slicing](block-integrations/slant3d/slicing.md)
* [Slant3D Webhook](block-integrations/slant3d/webhook.md)
* [Smartlead Campaign](block-integrations/smartlead/campaign.md)
* [Stagehand Blocks](block-integrations/stagehand/blocks.md)
* [System Library Operations](block-integrations/system/library_operations.md)
* [System Store Operations](block-integrations/system/store_operations.md)
* [Text](block-integrations/text.md)
* [Todoist Comments](block-integrations/todoist/comments.md)
* [Todoist Labels](block-integrations/todoist/labels.md)
* [Todoist Projects](block-integrations/todoist/projects.md)
* [Todoist Sections](block-integrations/todoist/sections.md)
* [Todoist Tasks](block-integrations/todoist/tasks.md)
* [Twitter Blocks](block-integrations/twitter/blocks.md)
* [Twitter Bookmark](block-integrations/twitter/bookmark.md)
* [Twitter Follows](block-integrations/twitter/follows.md)
* [Twitter Hide](block-integrations/twitter/hide.md)
* [Twitter Like](block-integrations/twitter/like.md)
* [Twitter List Follows](block-integrations/twitter/list_follows.md)
* [Twitter List Lookup](block-integrations/twitter/list_lookup.md)
* [Twitter List Members](block-integrations/twitter/list_members.md)
* [Twitter List Tweets Lookup](block-integrations/twitter/list_tweets_lookup.md)
* [Twitter Manage](block-integrations/twitter/manage.md)
* [Twitter Manage Lists](block-integrations/twitter/manage_lists.md)
* [Twitter Mutes](block-integrations/twitter/mutes.md)
* [Twitter Pinned Lists](block-integrations/twitter/pinned_lists.md)
* [Twitter Quote](block-integrations/twitter/quote.md)
* [Twitter Retweet](block-integrations/twitter/retweet.md)
* [Twitter Search Spaces](block-integrations/twitter/search_spaces.md)
* [Twitter Spaces Lookup](block-integrations/twitter/spaces_lookup.md)
* [Twitter Timeline](block-integrations/twitter/timeline.md)
* [Twitter Tweet Lookup](block-integrations/twitter/tweet_lookup.md)
* [Twitter User Lookup](block-integrations/twitter/user_lookup.md)
* [Wolfram LLM API](block-integrations/wolfram/llm_api.md)
* [Zerobounce Validate Emails](block-integrations/zerobounce/validate_emails.md)

Some files were not shown because too many files have changed in this diff Show More