Compare commits
2 Commits
gitbook
...
fix/chat-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e62f43652 | ||
|
|
49e0fb5f40 |
@@ -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._",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 504 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 68 KiB |
@@ -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)
|
|
||||||