mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
merge: resolve conflict in feature flags
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import fastapi
|
||||
import fastapi.testclient
|
||||
import pytest
|
||||
|
||||
from backend.api.features.v1 import v1_router
|
||||
|
||||
app = fastapi.FastAPI()
|
||||
app.include_router(v1_router)
|
||||
client = fastapi.testclient.TestClient(app)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_app_auth(mock_jwt_user):
|
||||
from autogpt_libs.auth.jwt_utils import get_jwt_payload
|
||||
|
||||
app.dependency_overrides[get_jwt_payload] = mock_jwt_user["get_jwt_payload"]
|
||||
yield
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
def test_onboarding_profile_success(mocker):
|
||||
mock_extract = mocker.patch(
|
||||
"backend.api.features.v1.extract_business_understanding",
|
||||
new_callable=AsyncMock,
|
||||
)
|
||||
mock_upsert = mocker.patch(
|
||||
"backend.api.features.v1.upsert_business_understanding",
|
||||
new_callable=AsyncMock,
|
||||
)
|
||||
|
||||
from backend.data.understanding import BusinessUnderstandingInput
|
||||
|
||||
mock_extract.return_value = BusinessUnderstandingInput.model_construct(
|
||||
user_name="John",
|
||||
user_role="Founder/CEO",
|
||||
pain_points=["Finding leads"],
|
||||
suggested_prompts={"Learn": ["How do I automate lead gen?"]},
|
||||
)
|
||||
mock_upsert.return_value = AsyncMock()
|
||||
|
||||
response = client.post(
|
||||
"/onboarding/profile",
|
||||
json={
|
||||
"user_name": "John",
|
||||
"user_role": "Founder/CEO",
|
||||
"pain_points": ["Finding leads", "Email & outreach"],
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
mock_extract.assert_awaited_once()
|
||||
mock_upsert.assert_awaited_once()
|
||||
|
||||
|
||||
def test_onboarding_profile_missing_fields():
|
||||
response = client.post(
|
||||
"/onboarding/profile",
|
||||
json={"user_name": "John"},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
@@ -63,12 +63,17 @@ from backend.data.onboarding import (
|
||||
UserOnboardingUpdate,
|
||||
complete_onboarding_step,
|
||||
complete_re_run_agent,
|
||||
format_onboarding_for_extraction,
|
||||
get_recommended_agents,
|
||||
get_user_onboarding,
|
||||
onboarding_enabled,
|
||||
reset_user_onboarding,
|
||||
update_user_onboarding,
|
||||
)
|
||||
from backend.data.tally import extract_business_understanding
|
||||
from backend.data.understanding import (
|
||||
BusinessUnderstandingInput,
|
||||
upsert_business_understanding,
|
||||
)
|
||||
from backend.data.user import (
|
||||
get_or_create_user,
|
||||
get_user_by_id,
|
||||
@@ -282,35 +287,33 @@ async def get_onboarding_agents(
|
||||
return await get_recommended_agents(user_id)
|
||||
|
||||
|
||||
class OnboardingStatusResponse(pydantic.BaseModel):
|
||||
"""Response for onboarding status check."""
|
||||
class OnboardingProfileRequest(pydantic.BaseModel):
|
||||
"""Request body for onboarding profile submission."""
|
||||
|
||||
is_onboarding_enabled: bool
|
||||
is_chat_enabled: bool
|
||||
user_name: str = pydantic.Field(min_length=1, max_length=100)
|
||||
user_role: str = pydantic.Field(min_length=1, max_length=100)
|
||||
pain_points: list[str] = pydantic.Field(default_factory=list, max_length=20)
|
||||
|
||||
|
||||
class OnboardingStatusResponse(pydantic.BaseModel):
|
||||
"""Response for onboarding completion check."""
|
||||
|
||||
is_completed: bool
|
||||
|
||||
|
||||
@v1_router.get(
|
||||
"/onboarding/enabled",
|
||||
summary="Is onboarding enabled",
|
||||
"/onboarding/completed",
|
||||
summary="Check if onboarding is completed",
|
||||
tags=["onboarding", "public"],
|
||||
response_model=OnboardingStatusResponse,
|
||||
dependencies=[Security(requires_user)],
|
||||
)
|
||||
async def is_onboarding_enabled(
|
||||
async def is_onboarding_completed(
|
||||
user_id: Annotated[str, Security(get_user_id)],
|
||||
) -> OnboardingStatusResponse:
|
||||
# Check if chat is enabled for user
|
||||
is_chat_enabled = await is_feature_enabled(Flag.CHAT, user_id, False)
|
||||
|
||||
# If chat is enabled, skip legacy onboarding
|
||||
if is_chat_enabled:
|
||||
return OnboardingStatusResponse(
|
||||
is_onboarding_enabled=False,
|
||||
is_chat_enabled=True,
|
||||
)
|
||||
|
||||
user_onboarding = await get_user_onboarding(user_id)
|
||||
return OnboardingStatusResponse(
|
||||
is_onboarding_enabled=await onboarding_enabled(),
|
||||
is_chat_enabled=False,
|
||||
is_completed=OnboardingStep.VISIT_COPILOT in user_onboarding.completedSteps,
|
||||
)
|
||||
|
||||
|
||||
@@ -325,6 +328,38 @@ async def reset_onboarding(user_id: Annotated[str, Security(get_user_id)]):
|
||||
return await reset_user_onboarding(user_id)
|
||||
|
||||
|
||||
@v1_router.post(
|
||||
"/onboarding/profile",
|
||||
summary="Submit onboarding profile",
|
||||
tags=["onboarding"],
|
||||
dependencies=[Security(requires_user)],
|
||||
)
|
||||
async def submit_onboarding_profile(
|
||||
data: OnboardingProfileRequest,
|
||||
user_id: Annotated[str, Security(get_user_id)],
|
||||
):
|
||||
formatted = format_onboarding_for_extraction(
|
||||
user_name=data.user_name,
|
||||
user_role=data.user_role,
|
||||
pain_points=data.pain_points,
|
||||
)
|
||||
|
||||
try:
|
||||
understanding_input = await extract_business_understanding(formatted)
|
||||
except Exception:
|
||||
understanding_input = BusinessUnderstandingInput.model_construct()
|
||||
|
||||
# Ensure the direct fields are set even if LLM missed them
|
||||
understanding_input.user_name = data.user_name
|
||||
understanding_input.user_role = data.user_role
|
||||
if not understanding_input.pain_points:
|
||||
understanding_input.pain_points = data.pain_points
|
||||
|
||||
await upsert_business_understanding(user_id, understanding_input)
|
||||
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
########################################################
|
||||
##################### Blocks ###########################
|
||||
########################################################
|
||||
|
||||
@@ -730,11 +730,7 @@ async def stream_chat_completion_baseline(
|
||||
# Without this, mode-switching after a failed turn would lose
|
||||
# the partial assistant response from the transcript.
|
||||
if _stream_error and state.assistant_text:
|
||||
_last_is_asst = (
|
||||
transcript_builder.entry_count > 0
|
||||
and transcript_builder._entries[-1].type == "assistant"
|
||||
)
|
||||
if not _last_is_asst:
|
||||
if transcript_builder.last_entry_type != "assistant":
|
||||
transcript_builder.append_assistant(
|
||||
content_blocks=[{"type": "text", "text": state.assistant_text}],
|
||||
model=config.fast_model,
|
||||
|
||||
@@ -233,3 +233,8 @@ class TranscriptBuilder:
|
||||
def is_empty(self) -> bool:
|
||||
"""Whether this builder has any entries."""
|
||||
return len(self._entries) == 0
|
||||
|
||||
@property
|
||||
def last_entry_type(self) -> str | None:
|
||||
"""Type of the last entry, or None if empty."""
|
||||
return self._entries[-1].type if self._entries else None
|
||||
|
||||
@@ -436,6 +436,28 @@ async def get_recommended_agents(user_id: str) -> list[StoreAgentDetails]:
|
||||
return [StoreAgentDetails.from_db(agent) for agent in recommended_agents]
|
||||
|
||||
|
||||
def format_onboarding_for_extraction(
|
||||
user_name: str,
|
||||
user_role: str,
|
||||
pain_points: list[str],
|
||||
) -> str:
|
||||
"""Format onboarding wizard answers as Q&A text for LLM extraction."""
|
||||
|
||||
def normalize(value: str) -> str:
|
||||
return " ".join(value.strip().split())
|
||||
|
||||
name = normalize(user_name)
|
||||
role = normalize(user_role)
|
||||
points = [normalize(p) for p in pain_points if normalize(p)]
|
||||
|
||||
lines = [
|
||||
f"Q: What is your name?\nA: {name}",
|
||||
f"Q: What best describes your role?\nA: {role}",
|
||||
f"Q: What tasks are eating your time?\nA: {', '.join(points)}",
|
||||
]
|
||||
return "\n\n".join(lines)
|
||||
|
||||
|
||||
@cached(maxsize=1, ttl_seconds=300) # Cache for 5 minutes since this rarely changes
|
||||
async def onboarding_enabled() -> bool:
|
||||
"""
|
||||
|
||||
27
autogpt_platform/backend/backend/data/onboarding_test.py
Normal file
27
autogpt_platform/backend/backend/data/onboarding_test.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from backend.data.onboarding import format_onboarding_for_extraction
|
||||
|
||||
|
||||
def test_format_onboarding_for_extraction_basic():
|
||||
result = format_onboarding_for_extraction(
|
||||
user_name="John",
|
||||
user_role="Founder/CEO",
|
||||
pain_points=["Finding leads", "Email & outreach"],
|
||||
)
|
||||
assert "Q: What is your name?" in result
|
||||
assert "A: John" in result
|
||||
assert "Q: What best describes your role?" in result
|
||||
assert "A: Founder/CEO" in result
|
||||
assert "Q: What tasks are eating your time?" in result
|
||||
assert "Finding leads" in result
|
||||
assert "Email & outreach" in result
|
||||
|
||||
|
||||
def test_format_onboarding_for_extraction_with_other():
|
||||
result = format_onboarding_for_extraction(
|
||||
user_name="Jane",
|
||||
user_role="Data Scientist",
|
||||
pain_points=["Research", "Building dashboards"],
|
||||
)
|
||||
assert "A: Jane" in result
|
||||
assert "A: Data Scientist" in result
|
||||
assert "Research, Building dashboards" in result
|
||||
@@ -122,6 +122,7 @@
|
||||
"tailwind-merge": "2.6.0",
|
||||
"tailwind-scrollbar": "3.1.0",
|
||||
"tailwindcss-animate": "1.0.7",
|
||||
"twemoji": "14.0.2",
|
||||
"use-stick-to-bottom": "1.1.2",
|
||||
"uuid": "11.1.0",
|
||||
"vaul": "1.1.2",
|
||||
@@ -150,6 +151,7 @@
|
||||
"@types/react-modal": "3.16.3",
|
||||
"@types/react-window": "2.0.0",
|
||||
"@vitejs/plugin-react": "5.1.2",
|
||||
"@vitest/coverage-v8": "4.0.17",
|
||||
"axe-playwright": "2.2.2",
|
||||
"chromatic": "13.3.3",
|
||||
"concurrently": "9.2.1",
|
||||
@@ -171,7 +173,6 @@
|
||||
"tailwindcss": "3.4.17",
|
||||
"typescript": "5.9.3",
|
||||
"vite-tsconfig-paths": "6.0.4",
|
||||
"@vitest/coverage-v8": "4.0.17",
|
||||
"vitest": "4.0.17"
|
||||
},
|
||||
"msw": {
|
||||
|
||||
50
autogpt_platform/frontend/pnpm-lock.yaml
generated
50
autogpt_platform/frontend/pnpm-lock.yaml
generated
@@ -288,6 +288,9 @@ importers:
|
||||
tailwindcss-animate:
|
||||
specifier: 1.0.7
|
||||
version: 1.0.7(tailwindcss@3.4.17)
|
||||
twemoji:
|
||||
specifier: 14.0.2
|
||||
version: 14.0.2
|
||||
use-stick-to-bottom:
|
||||
specifier: 1.1.2
|
||||
version: 1.1.2(react@18.3.1)
|
||||
@@ -5498,6 +5501,10 @@ packages:
|
||||
resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==}
|
||||
engines: {node: '>=14.14'}
|
||||
|
||||
fs-extra@8.1.0:
|
||||
resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
|
||||
engines: {node: '>=6 <7 || >=8'}
|
||||
|
||||
fs-monkey@1.1.0:
|
||||
resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==}
|
||||
|
||||
@@ -6149,6 +6156,12 @@ packages:
|
||||
jsonc-parser@2.2.1:
|
||||
resolution: {integrity: sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==}
|
||||
|
||||
jsonfile@4.0.0:
|
||||
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
|
||||
|
||||
jsonfile@5.0.0:
|
||||
resolution: {integrity: sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==}
|
||||
|
||||
jsonfile@6.2.0:
|
||||
resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
|
||||
|
||||
@@ -8218,6 +8231,12 @@ packages:
|
||||
tty-browserify@0.0.1:
|
||||
resolution: {integrity: sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==}
|
||||
|
||||
twemoji-parser@14.0.0:
|
||||
resolution: {integrity: sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA==}
|
||||
|
||||
twemoji@14.0.2:
|
||||
resolution: {integrity: sha512-BzOoXIe1QVdmsUmZ54xbEH+8AgtOKUiG53zO5vVP2iUu6h5u9lN15NcuS6te4OY96qx0H7JK9vjjl9WQbkTRuA==}
|
||||
|
||||
type-check@0.4.0:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -8342,6 +8361,10 @@ packages:
|
||||
unist-util-visit@5.0.0:
|
||||
resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
|
||||
|
||||
universalify@0.1.2:
|
||||
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
|
||||
engines: {node: '>= 4.0.0'}
|
||||
|
||||
universalify@2.0.1:
|
||||
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
@@ -14595,6 +14618,12 @@ snapshots:
|
||||
jsonfile: 6.2.0
|
||||
universalify: 2.0.1
|
||||
|
||||
fs-extra@8.1.0:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
jsonfile: 4.0.0
|
||||
universalify: 0.1.2
|
||||
|
||||
fs-monkey@1.1.0: {}
|
||||
|
||||
fs.realpath@1.0.0: {}
|
||||
@@ -15333,6 +15362,16 @@ snapshots:
|
||||
|
||||
jsonc-parser@2.2.1: {}
|
||||
|
||||
jsonfile@4.0.0:
|
||||
optionalDependencies:
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
jsonfile@5.0.0:
|
||||
dependencies:
|
||||
universalify: 0.1.2
|
||||
optionalDependencies:
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
jsonfile@6.2.0:
|
||||
dependencies:
|
||||
universalify: 2.0.1
|
||||
@@ -17893,6 +17932,15 @@ snapshots:
|
||||
|
||||
tty-browserify@0.0.1: {}
|
||||
|
||||
twemoji-parser@14.0.0: {}
|
||||
|
||||
twemoji@14.0.2:
|
||||
dependencies:
|
||||
fs-extra: 8.1.0
|
||||
jsonfile: 5.0.0
|
||||
twemoji-parser: 14.0.0
|
||||
universalify: 0.1.2
|
||||
|
||||
type-check@0.4.0:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
@@ -18030,6 +18078,8 @@ snapshots:
|
||||
unist-util-is: 6.0.1
|
||||
unist-util-visit-parents: 6.0.2
|
||||
|
||||
universalify@0.1.2: {}
|
||||
|
||||
universalify@2.0.1: {}
|
||||
|
||||
unplugin@1.0.1:
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
"use client";
|
||||
import { OnboardingText } from "../components/OnboardingText";
|
||||
import OnboardingButton from "../components/OnboardingButton";
|
||||
import Image from "next/image";
|
||||
import { useOnboarding } from "../../../../providers/onboarding/onboarding-provider";
|
||||
|
||||
export default function Page() {
|
||||
useOnboarding(1);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Image
|
||||
src="/gpt_dark_RGB.svg"
|
||||
alt="GPT Dark Logo"
|
||||
className="-mb-2"
|
||||
width={300}
|
||||
height={300}
|
||||
/>
|
||||
<OnboardingText className="mb-3" variant="header" center>
|
||||
Welcome to AutoGPT
|
||||
</OnboardingText>
|
||||
<OnboardingText className="mb-12" center>
|
||||
Think of AutoGPT as your digital teammate, working intelligently to
|
||||
<br />
|
||||
complete tasks based on your directions. Let's learn a bit about
|
||||
you to
|
||||
<br />
|
||||
tailor your experience.
|
||||
</OnboardingText>
|
||||
<OnboardingButton href="/onboarding/2-reason">Continue</OnboardingButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
"use client";
|
||||
import OnboardingButton from "../components/OnboardingButton";
|
||||
import {
|
||||
OnboardingFooter,
|
||||
OnboardingHeader,
|
||||
OnboardingStep,
|
||||
} from "../components/OnboardingStep";
|
||||
import { OnboardingText } from "../components/OnboardingText";
|
||||
import OnboardingList from "../components/OnboardingList";
|
||||
import { isEmptyOrWhitespace } from "@/lib/utils";
|
||||
import { useOnboarding } from "../../../../providers/onboarding/onboarding-provider";
|
||||
|
||||
const reasons = [
|
||||
{
|
||||
label: "Content & Marketing",
|
||||
text: "Content creation, social media management, blogging, creative writing",
|
||||
id: "content_marketing",
|
||||
},
|
||||
{
|
||||
label: "Business & Workflow Automation",
|
||||
text: "Operations, task management, productivity",
|
||||
id: "business_workflow_automation",
|
||||
},
|
||||
{
|
||||
label: "Data & Research",
|
||||
text: "Data analysis, insights, research, financial operation",
|
||||
id: "data_research",
|
||||
},
|
||||
{
|
||||
label: "AI & Innovation",
|
||||
text: "AI experimentation, automation testing, advanced AI applications",
|
||||
id: "ai_innovation",
|
||||
},
|
||||
{
|
||||
label: "Personal productivity",
|
||||
text: "Automating daily tasks, organizing information, personal workflows",
|
||||
id: "personal_productivity",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Page() {
|
||||
const { state, updateState } = useOnboarding(2, "WELCOME");
|
||||
|
||||
return (
|
||||
<OnboardingStep>
|
||||
<OnboardingHeader backHref={"/onboarding/1-welcome"}>
|
||||
<OnboardingText className="mt-4" variant="header" center>
|
||||
What's your main reason for using AutoGPT?
|
||||
</OnboardingText>
|
||||
<OnboardingText className="mt-1" center>
|
||||
Select the option that best matches your needs
|
||||
</OnboardingText>
|
||||
</OnboardingHeader>
|
||||
<OnboardingList
|
||||
elements={reasons}
|
||||
selectedId={state?.usageReason}
|
||||
onSelect={(usageReason) => updateState({ usageReason })}
|
||||
/>
|
||||
<OnboardingFooter>
|
||||
<OnboardingButton
|
||||
href="/onboarding/3-services"
|
||||
disabled={isEmptyOrWhitespace(state?.usageReason)}
|
||||
>
|
||||
Next
|
||||
</OnboardingButton>
|
||||
</OnboardingFooter>
|
||||
</OnboardingStep>
|
||||
);
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
"use client";
|
||||
import OnboardingButton from "../components/OnboardingButton";
|
||||
import {
|
||||
OnboardingStep,
|
||||
OnboardingHeader,
|
||||
OnboardingFooter,
|
||||
} from "../components/OnboardingStep";
|
||||
import { OnboardingText } from "../components/OnboardingText";
|
||||
import { OnboardingGrid } from "../components/OnboardingGrid";
|
||||
import { useCallback } from "react";
|
||||
import OnboardingInput from "../components/OnboardingInput";
|
||||
import { useOnboarding } from "../../../../providers/onboarding/onboarding-provider";
|
||||
|
||||
const services = [
|
||||
{
|
||||
name: "D-ID",
|
||||
text: "Generate AI-powered avatars and videos for dynamic content creation.",
|
||||
icon: "/integrations/d-id.png",
|
||||
},
|
||||
{
|
||||
name: "Discord",
|
||||
text: "A chat platform for communities and teams, supporting text, voice, and video.",
|
||||
icon: "/integrations/discord.png",
|
||||
},
|
||||
{
|
||||
name: "GitHub",
|
||||
text: "AutoGPT can track issues, manage repos, and automate workflows with GitHub.",
|
||||
icon: "/integrations/github.png",
|
||||
},
|
||||
{
|
||||
name: "Google Workspace",
|
||||
text: "Automate emails, calendar events, and document management in AutoGPT with Google Workspace.",
|
||||
icon: "/integrations/google.png",
|
||||
},
|
||||
{
|
||||
name: "Google Maps",
|
||||
text: "Fetch locations, directions, and real-time geodata for navigation.",
|
||||
icon: "/integrations/maps.png",
|
||||
},
|
||||
{
|
||||
name: "HubSpot",
|
||||
text: "Manage customer relationships, automate marketing, and track sales.",
|
||||
icon: "/integrations/hubspot.png",
|
||||
},
|
||||
{
|
||||
name: "Linear",
|
||||
text: "Streamline project management and issue tracking with a modern workflow.",
|
||||
icon: "/integrations/linear.png",
|
||||
},
|
||||
{
|
||||
name: "Medium",
|
||||
text: "Publish and explore insightful content with a powerful writing platform.",
|
||||
icon: "/integrations/medium.png",
|
||||
},
|
||||
{
|
||||
name: "Mem0",
|
||||
text: "AI-powered memory assistant for smarter data organization and recall.",
|
||||
icon: "/integrations/mem0.png",
|
||||
},
|
||||
{
|
||||
name: "Notion",
|
||||
text: "Organize work, notes, and databases in an all-in-one workspace.",
|
||||
icon: "/integrations/notion.png",
|
||||
},
|
||||
{
|
||||
name: "NVIDIA",
|
||||
text: "Accelerate AI, graphics, and computing with cutting-edge technology.",
|
||||
icon: "/integrations/nvidia.jpg",
|
||||
},
|
||||
{
|
||||
name: "OpenWeatherMap",
|
||||
text: "Access real-time weather data and forecasts worldwide.",
|
||||
icon: "/integrations/openweathermap.png",
|
||||
},
|
||||
{
|
||||
name: "Pinecone",
|
||||
text: "Store and search vector data for AI-driven applications.",
|
||||
icon: "/integrations/pinecone.png",
|
||||
},
|
||||
{
|
||||
name: "Reddit",
|
||||
text: "Explore trending discussions and engage with online communities.",
|
||||
icon: "/integrations/reddit.png",
|
||||
},
|
||||
{
|
||||
name: "Slant3D",
|
||||
text: "Automate and optimize 3D printing workflows with AI.",
|
||||
icon: "/integrations/slant3d.jpeg",
|
||||
},
|
||||
{
|
||||
name: "SMTP",
|
||||
text: "Send and manage emails with secure and reliable delivery.",
|
||||
icon: "/integrations/smtp.png",
|
||||
},
|
||||
{
|
||||
name: "Todoist",
|
||||
text: "Organize tasks and projects with a simple, intuitive to-do list.",
|
||||
icon: "/integrations/todoist.png",
|
||||
},
|
||||
{
|
||||
name: "Twitter (X)",
|
||||
text: "Stay connected and share updates on the world's biggest conversation platform.",
|
||||
icon: "/integrations/x.png",
|
||||
},
|
||||
{
|
||||
name: "Unreal Speech",
|
||||
text: "Generate natural-sounding AI voices for speech applications.",
|
||||
icon: "/integrations/unreal-speech.png",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Page() {
|
||||
const { state, updateState } = useOnboarding(3, "USAGE_REASON");
|
||||
|
||||
const switchIntegration = useCallback(
|
||||
(name: string) => {
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
|
||||
const integrations = state.integrations.includes(name)
|
||||
? state.integrations.filter((i) => i !== name)
|
||||
: [...state.integrations, name];
|
||||
|
||||
updateState({ integrations });
|
||||
},
|
||||
[state, updateState],
|
||||
);
|
||||
|
||||
return (
|
||||
<OnboardingStep>
|
||||
<OnboardingHeader backHref={"/onboarding/2-reason"}>
|
||||
<OnboardingText className="mt-4" variant="header" center>
|
||||
What platforms or services would you like AutoGPT to work with?
|
||||
</OnboardingText>
|
||||
<OnboardingText className="mt-1" center>
|
||||
You can select more than one option
|
||||
</OnboardingText>
|
||||
</OnboardingHeader>
|
||||
|
||||
<div className="w-fit">
|
||||
<OnboardingText className="my-4" variant="subheader">
|
||||
Available integrations
|
||||
</OnboardingText>
|
||||
<OnboardingGrid
|
||||
elements={services}
|
||||
selected={state?.integrations}
|
||||
onSelect={switchIntegration}
|
||||
/>
|
||||
<OnboardingText className="mt-12" variant="subheader">
|
||||
Help us grow our integrations
|
||||
</OnboardingText>
|
||||
<OnboardingText className="my-4">
|
||||
Let us know which partnerships you'd like to see next
|
||||
</OnboardingText>
|
||||
<OnboardingInput
|
||||
className="mb-4"
|
||||
placeholder="Others (please specify)"
|
||||
value={state?.otherIntegrations || ""}
|
||||
onChange={(otherIntegrations) => updateState({ otherIntegrations })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<OnboardingFooter>
|
||||
<OnboardingButton className="mb-2" href="/onboarding/4-agent">
|
||||
Next
|
||||
</OnboardingButton>
|
||||
</OnboardingFooter>
|
||||
</OnboardingStep>
|
||||
);
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
"use client";
|
||||
import { isEmptyOrWhitespace } from "@/lib/utils";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useOnboarding } from "../../../../providers/onboarding/onboarding-provider";
|
||||
import OnboardingAgentCard from "../components/OnboardingAgentCard";
|
||||
import OnboardingButton from "../components/OnboardingButton";
|
||||
import {
|
||||
OnboardingFooter,
|
||||
OnboardingHeader,
|
||||
OnboardingStep,
|
||||
} from "../components/OnboardingStep";
|
||||
import { OnboardingText } from "../components/OnboardingText";
|
||||
import { getV1RecommendedOnboardingAgents } from "@/app/api/__generated__/endpoints/onboarding/onboarding";
|
||||
import { resolveResponse } from "@/app/api/helpers";
|
||||
import { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
|
||||
|
||||
export default function Page() {
|
||||
const { state, updateState, completeStep } = useOnboarding(4, "INTEGRATIONS");
|
||||
const [agents, setAgents] = useState<StoreAgentDetails[]>([]);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
resolveResponse(getV1RecommendedOnboardingAgents()).then((agents) => {
|
||||
if (agents.length < 2) {
|
||||
completeStep("CONGRATS");
|
||||
router.replace("/");
|
||||
}
|
||||
setAgents(agents);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Deselect agent if it's not in the list of agents
|
||||
if (
|
||||
state?.selectedStoreListingVersionId &&
|
||||
agents.length > 0 &&
|
||||
!agents.some(
|
||||
(agent) =>
|
||||
agent.store_listing_version_id ===
|
||||
state.selectedStoreListingVersionId,
|
||||
)
|
||||
) {
|
||||
updateState({
|
||||
selectedStoreListingVersionId: null,
|
||||
agentInput: {},
|
||||
});
|
||||
}
|
||||
}, [state?.selectedStoreListingVersionId, updateState, agents]);
|
||||
|
||||
return (
|
||||
<OnboardingStep>
|
||||
<OnboardingHeader backHref={"/onboarding/3-services"}>
|
||||
<OnboardingText className="mt-4" variant="header" center>
|
||||
Choose an agent
|
||||
</OnboardingText>
|
||||
<OnboardingText className="mt-1" center>
|
||||
We think these agents are a good match for you based on your answers
|
||||
</OnboardingText>
|
||||
</OnboardingHeader>
|
||||
|
||||
<div className="my-12 flex items-center justify-between gap-5">
|
||||
<OnboardingAgentCard
|
||||
agent={agents[0]}
|
||||
selected={
|
||||
agents[0] !== undefined
|
||||
? state?.selectedStoreListingVersionId ==
|
||||
agents[0]?.store_listing_version_id
|
||||
: false
|
||||
}
|
||||
onClick={() =>
|
||||
updateState({
|
||||
selectedStoreListingVersionId: agents[0].store_listing_version_id,
|
||||
agentInput: {},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<OnboardingAgentCard
|
||||
agent={agents[1]}
|
||||
selected={
|
||||
agents[1] !== undefined
|
||||
? state?.selectedStoreListingVersionId ==
|
||||
agents[1]?.store_listing_version_id
|
||||
: false
|
||||
}
|
||||
onClick={() =>
|
||||
updateState({
|
||||
selectedStoreListingVersionId: agents[1].store_listing_version_id,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<OnboardingFooter>
|
||||
<OnboardingButton
|
||||
href="/onboarding/5-run"
|
||||
disabled={isEmptyOrWhitespace(state?.selectedStoreListingVersionId)}
|
||||
>
|
||||
Next
|
||||
</OnboardingButton>
|
||||
</OnboardingFooter>
|
||||
</OnboardingStep>
|
||||
);
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { CredentialsMetaInput } from "@/app/api/__generated__/models/credentialsMetaInput";
|
||||
import { GraphModel } from "@/app/api/__generated__/models/graphModel";
|
||||
import { CredentialsInput } from "@/components/contextual/CredentialsInput/CredentialsInput";
|
||||
import { useState } from "react";
|
||||
import { getSchemaDefaultCredentials } from "../../helpers";
|
||||
import { areAllCredentialsSet, getCredentialFields } from "./helpers";
|
||||
|
||||
type Credential = CredentialsMetaInput | undefined;
|
||||
type Credentials = Record<string, Credential>;
|
||||
|
||||
type Props = {
|
||||
agent: GraphModel | null;
|
||||
siblingInputs?: Record<string, any>;
|
||||
onCredentialsChange: (
|
||||
credentials: Record<string, CredentialsMetaInput>,
|
||||
) => void;
|
||||
onValidationChange: (isValid: boolean) => void;
|
||||
onLoadingChange: (isLoading: boolean) => void;
|
||||
};
|
||||
|
||||
export function AgentOnboardingCredentials(props: Props) {
|
||||
const [inputCredentials, setInputCredentials] = useState<Credentials>({});
|
||||
|
||||
const fields = getCredentialFields(props.agent);
|
||||
const required = Object.keys(fields || {}).length > 0;
|
||||
|
||||
if (!required) return null;
|
||||
|
||||
function handleSelectCredentials(key: string, value: Credential) {
|
||||
const updated = { ...inputCredentials, [key]: value };
|
||||
setInputCredentials(updated);
|
||||
|
||||
const sanitized: Record<string, CredentialsMetaInput> = {};
|
||||
for (const [k, v] of Object.entries(updated)) {
|
||||
if (v) sanitized[k] = v;
|
||||
}
|
||||
|
||||
props.onCredentialsChange(sanitized);
|
||||
|
||||
const isValid = !required || areAllCredentialsSet(fields, updated);
|
||||
props.onValidationChange(isValid);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.entries(fields).map(([key, inputSubSchema]) => (
|
||||
<div key={key} className="mt-4">
|
||||
<CredentialsInput
|
||||
schema={inputSubSchema}
|
||||
selectedCredentials={
|
||||
inputCredentials[key] ??
|
||||
getSchemaDefaultCredentials(inputSubSchema)
|
||||
}
|
||||
onSelectCredentials={(value) => handleSelectCredentials(key, value)}
|
||||
siblingInputs={props.siblingInputs}
|
||||
onLoaded={(loaded) => props.onLoadingChange(!loaded)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { CredentialsMetaInput } from "@/app/api/__generated__/models/credentialsMetaInput";
|
||||
import { GraphModel } from "@/app/api/__generated__/models/graphModel";
|
||||
import { BlockIOCredentialsSubSchema } from "@/lib/autogpt-server-api/types";
|
||||
|
||||
export function getCredentialFields(
|
||||
agent: GraphModel | null,
|
||||
): AgentCredentialsFields {
|
||||
if (!agent) return {};
|
||||
|
||||
const hasNoInputs =
|
||||
!agent.credentials_input_schema ||
|
||||
typeof agent.credentials_input_schema !== "object" ||
|
||||
!("properties" in agent.credentials_input_schema) ||
|
||||
!agent.credentials_input_schema.properties;
|
||||
|
||||
if (hasNoInputs) return {};
|
||||
|
||||
return agent.credentials_input_schema.properties as AgentCredentialsFields;
|
||||
}
|
||||
|
||||
export type AgentCredentialsFields = Record<
|
||||
string,
|
||||
BlockIOCredentialsSubSchema
|
||||
>;
|
||||
|
||||
export function areAllCredentialsSet(
|
||||
fields: AgentCredentialsFields,
|
||||
inputs: Record<string, CredentialsMetaInput | undefined>,
|
||||
) {
|
||||
const required = Object.keys(fields || {});
|
||||
return required.every((k) => Boolean(inputs[k]));
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { OnboardingText } from "../../components/OnboardingText";
|
||||
|
||||
type RunAgentHintProps = {
|
||||
handleNewRun: () => void;
|
||||
};
|
||||
|
||||
export function RunAgentHint(props: RunAgentHintProps) {
|
||||
return (
|
||||
<div className="ml-[104px] w-[481px] pl-5">
|
||||
<div className="flex flex-col">
|
||||
<OnboardingText variant="header">Run your first agent</OnboardingText>
|
||||
<span className="mt-9 text-base font-normal leading-normal text-zinc-600">
|
||||
A 'run' is when your agent starts working on a task
|
||||
</span>
|
||||
<span className="mt-4 text-base font-normal leading-normal text-zinc-600">
|
||||
Click on <b>New Run</b> below to try it out
|
||||
</span>
|
||||
|
||||
<div
|
||||
onClick={props.handleNewRun}
|
||||
className={cn(
|
||||
"mt-16 flex h-[68px] w-[330px] items-center justify-center rounded-xl border-2 border-violet-700 bg-neutral-50",
|
||||
"cursor-pointer transition-all duration-200 ease-in-out hover:bg-violet-50",
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
width="38"
|
||||
height="38"
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g stroke="#6d28d9" strokeWidth="1.2" strokeLinecap="round">
|
||||
<line x1="16" y1="8" x2="16" y2="24" />
|
||||
<line x1="8" y1="16" x2="24" y2="16" />
|
||||
</g>
|
||||
</svg>
|
||||
<span className="ml-3 font-sans text-[19px] font-medium leading-normal text-violet-700">
|
||||
New run
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
|
||||
import StarRating from "../../components/StarRating";
|
||||
import SmartImage from "@/components/__legacy__/SmartImage";
|
||||
|
||||
type Props = {
|
||||
storeAgent: StoreAgentDetails | null;
|
||||
};
|
||||
|
||||
export function SelectedAgentCard(props: Props) {
|
||||
return (
|
||||
<div className="fixed left-1/4 top-1/2 w-[481px] -translate-x-1/2 -translate-y-1/2">
|
||||
<div className="h-[156px] w-[481px] rounded-xl bg-white px-6 pb-5 pt-4">
|
||||
<span className="font-sans text-xs font-medium tracking-wide text-zinc-500">
|
||||
SELECTED AGENT
|
||||
</span>
|
||||
{props.storeAgent ? (
|
||||
<div className="mt-4 flex h-20 rounded-lg bg-violet-50 p-3">
|
||||
{/* Left image */}
|
||||
<SmartImage
|
||||
src={props.storeAgent.agent_image[0]}
|
||||
alt="Agent cover"
|
||||
className="w-[350px] rounded-lg"
|
||||
/>
|
||||
{/* Right content */}
|
||||
<div className="ml-3 flex flex-1 flex-col">
|
||||
<div className="mb-2 flex flex-col items-start">
|
||||
<span className="data-sentry-unmask w-[292px] truncate font-sans text-[14px] font-medium leading-tight text-zinc-800">
|
||||
{props.storeAgent.agent_name}
|
||||
</span>
|
||||
<span className="data-sentry-unmask font-norma w-[292px] truncate font-sans text-xs text-zinc-600">
|
||||
by {props.storeAgent.creator}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex w-[292px] items-center justify-between">
|
||||
<span className="truncate font-sans text-xs font-normal leading-tight text-zinc-600">
|
||||
{props.storeAgent.runs.toLocaleString("en-US")} runs
|
||||
</span>
|
||||
<StarRating
|
||||
className="font-sans text-xs font-normal leading-tight text-zinc-600"
|
||||
starSize={12}
|
||||
rating={props.storeAgent.rating || 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 flex h-20 animate-pulse rounded-lg bg-gray-300 p-2" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import type {
|
||||
BlockIOCredentialsSubSchema,
|
||||
CredentialsMetaInput,
|
||||
} from "@/lib/autogpt-server-api/types";
|
||||
import type { InputValues } from "./types";
|
||||
import { GraphModel } from "@/app/api/__generated__/models/graphModel";
|
||||
|
||||
export function computeInitialAgentInputs(
|
||||
agent: GraphModel | null,
|
||||
existingInputs?: InputValues | null,
|
||||
): InputValues {
|
||||
const properties = agent?.input_schema?.properties || {};
|
||||
const result: InputValues = {};
|
||||
|
||||
Object.entries(properties).forEach(([key, subSchema]) => {
|
||||
if (
|
||||
existingInputs &&
|
||||
key in existingInputs &&
|
||||
existingInputs[key] != null
|
||||
) {
|
||||
result[key] = existingInputs[key];
|
||||
return;
|
||||
}
|
||||
const def = (subSchema as unknown as { default?: string | number }).default;
|
||||
result[key] = def ?? "";
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
type IsRunDisabledParams = {
|
||||
agent: GraphModel | null;
|
||||
isRunning: boolean;
|
||||
agentInputs: InputValues | null | undefined;
|
||||
};
|
||||
|
||||
export function isRunDisabled({
|
||||
agent,
|
||||
isRunning,
|
||||
agentInputs,
|
||||
}: IsRunDisabledParams) {
|
||||
const hasEmptyInput = Object.values(agentInputs || {}).some(
|
||||
(value) => String(value).trim() === "",
|
||||
);
|
||||
|
||||
if (hasEmptyInput) return true;
|
||||
if (!agent) return true;
|
||||
if (isRunning) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getSchemaDefaultCredentials(
|
||||
schema: BlockIOCredentialsSubSchema,
|
||||
): CredentialsMetaInput | undefined {
|
||||
return schema.default as CredentialsMetaInput | undefined;
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/__legacy__/ui/card";
|
||||
import { RunAgentInputs } from "@/components/contextual/RunAgentInputs/RunAgentInputs";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { CircleNotchIcon } from "@phosphor-icons/react/dist/ssr";
|
||||
import { Play } from "lucide-react";
|
||||
import OnboardingButton from "../components/OnboardingButton";
|
||||
import { OnboardingHeader, OnboardingStep } from "../components/OnboardingStep";
|
||||
import { OnboardingText } from "../components/OnboardingText";
|
||||
import { AgentOnboardingCredentials } from "./components/AgentOnboardingCredentials/AgentOnboardingCredentials";
|
||||
import { RunAgentHint } from "./components/RunAgentHint";
|
||||
import { SelectedAgentCard } from "./components/SelectedAgentCard";
|
||||
import { isRunDisabled } from "./helpers";
|
||||
import type { InputValues } from "./types";
|
||||
import { useOnboardingRunStep } from "./useOnboardingRunStep";
|
||||
|
||||
export default function Page() {
|
||||
const {
|
||||
ready,
|
||||
error,
|
||||
showInput,
|
||||
agentGraph,
|
||||
onboarding,
|
||||
storeAgent,
|
||||
runningAgent,
|
||||
handleSetAgentInput,
|
||||
handleRunAgent,
|
||||
handleNewRun,
|
||||
handleCredentialsChange,
|
||||
handleCredentialsValidationChange,
|
||||
handleCredentialsLoadingChange,
|
||||
} = useOnboardingRunStep();
|
||||
|
||||
if (error) {
|
||||
return <ErrorCard responseError={error} />;
|
||||
}
|
||||
|
||||
if (!ready) {
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<CircleNotchIcon className="size-10 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<OnboardingStep dotted>
|
||||
<OnboardingHeader backHref={"/onboarding/4-agent"} transparent />
|
||||
<div className="flex min-h-[80vh] items-center justify-center">
|
||||
<SelectedAgentCard storeAgent={storeAgent} />
|
||||
<div className="w-[481px]" />
|
||||
{!showInput ? (
|
||||
<RunAgentHint handleNewRun={handleNewRun} />
|
||||
) : (
|
||||
<div className="ml-[104px] w-[481px] pl-5">
|
||||
<div className="flex flex-col">
|
||||
<OnboardingText variant="header">
|
||||
Provide details for your agent
|
||||
</OnboardingText>
|
||||
<span className="mt-9 text-base font-normal leading-normal text-zinc-600">
|
||||
Give your agent the details it needs to work—just enter <br />
|
||||
the key information and get started.
|
||||
</span>
|
||||
<span className="mt-4 text-base font-normal leading-normal text-zinc-600">
|
||||
When you're done, click <b>Run Agent</b>.
|
||||
</span>
|
||||
|
||||
<Card className="agpt-box mt-4">
|
||||
<CardHeader>
|
||||
<CardTitle className="font-poppins text-lg">Input</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{Object.entries(
|
||||
agentGraph?.input_schema.properties || {},
|
||||
).map(([key, inputSubSchema]) => (
|
||||
<RunAgentInputs
|
||||
key={key}
|
||||
schema={inputSubSchema}
|
||||
value={onboarding.state?.agentInput?.[key]}
|
||||
placeholder={inputSubSchema.description}
|
||||
onChange={(value) => handleSetAgentInput(key, value)}
|
||||
/>
|
||||
))}
|
||||
<AgentOnboardingCredentials
|
||||
agent={agentGraph}
|
||||
siblingInputs={
|
||||
(onboarding.state?.agentInput as Record<string, any>) ||
|
||||
undefined
|
||||
}
|
||||
onCredentialsChange={handleCredentialsChange}
|
||||
onValidationChange={handleCredentialsValidationChange}
|
||||
onLoadingChange={handleCredentialsLoadingChange}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<OnboardingButton
|
||||
variant="violet"
|
||||
className="mt-8 w-[136px]"
|
||||
loading={runningAgent}
|
||||
disabled={isRunDisabled({
|
||||
agent: agentGraph,
|
||||
isRunning: runningAgent,
|
||||
agentInputs:
|
||||
(onboarding.state?.agentInput as unknown as InputValues) ||
|
||||
null,
|
||||
})}
|
||||
onClick={handleRunAgent}
|
||||
icon={<Play className="mr-2" size={18} />}
|
||||
>
|
||||
Run agent
|
||||
</OnboardingButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</OnboardingStep>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export type InputPrimitive = string | number;
|
||||
export type InputValues = Record<string, InputPrimitive>;
|
||||
@@ -1,157 +0,0 @@
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { computeInitialAgentInputs } from "./helpers";
|
||||
import { InputValues } from "./types";
|
||||
import { okData, resolveResponse } from "@/app/api/helpers";
|
||||
import { postV2AddMarketplaceAgent } from "@/app/api/__generated__/endpoints/library/library";
|
||||
import {
|
||||
useGetV2GetAgentByVersion,
|
||||
useGetV2GetAgentGraph,
|
||||
} from "@/app/api/__generated__/endpoints/store/store";
|
||||
import { CredentialsMetaInput } from "@/app/api/__generated__/models/credentialsMetaInput";
|
||||
import { GraphID } from "@/lib/autogpt-server-api";
|
||||
|
||||
export function useOnboardingRunStep() {
|
||||
const onboarding = useOnboarding(undefined, "AGENT_CHOICE");
|
||||
|
||||
const [showInput, setShowInput] = useState(false);
|
||||
const [runningAgent, setRunningAgent] = useState(false);
|
||||
|
||||
const [inputCredentials, setInputCredentials] = useState<
|
||||
Record<string, CredentialsMetaInput>
|
||||
>({});
|
||||
|
||||
const [credentialsValid, setCredentialsValid] = useState(true);
|
||||
const [credentialsLoaded, setCredentialsLoaded] = useState(false);
|
||||
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const api = useBackendAPI();
|
||||
|
||||
const currentAgentVersion =
|
||||
onboarding.state?.selectedStoreListingVersionId ?? "";
|
||||
|
||||
const {
|
||||
data: storeAgent,
|
||||
error: storeAgentQueryError,
|
||||
isSuccess: storeAgentQueryIsSuccess,
|
||||
} = useGetV2GetAgentByVersion(currentAgentVersion, {
|
||||
query: {
|
||||
enabled: !!currentAgentVersion,
|
||||
select: okData,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
data: agentGraphMeta,
|
||||
error: agentGraphQueryError,
|
||||
isSuccess: agentGraphQueryIsSuccess,
|
||||
} = useGetV2GetAgentGraph(currentAgentVersion, {
|
||||
query: {
|
||||
enabled: !!currentAgentVersion,
|
||||
select: okData,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onboarding.setStep(5);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (agentGraphMeta && onboarding.state) {
|
||||
const initialAgentInputs = computeInitialAgentInputs(
|
||||
agentGraphMeta,
|
||||
(onboarding.state.agentInput as unknown as InputValues) || null,
|
||||
);
|
||||
|
||||
onboarding.updateState({ agentInput: initialAgentInputs });
|
||||
}
|
||||
}, [agentGraphMeta]);
|
||||
|
||||
function handleNewRun() {
|
||||
if (!onboarding.state) return;
|
||||
|
||||
setShowInput(true);
|
||||
onboarding.setStep(6);
|
||||
onboarding.completeStep("AGENT_NEW_RUN");
|
||||
}
|
||||
|
||||
function handleSetAgentInput(key: string, value: string) {
|
||||
if (!onboarding.state) return;
|
||||
|
||||
onboarding.updateState({
|
||||
agentInput: {
|
||||
...onboarding.state.agentInput,
|
||||
[key]: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleRunAgent() {
|
||||
if (!agentGraphMeta || !storeAgent || !onboarding.state) {
|
||||
toast({
|
||||
title: "Error getting agent",
|
||||
description:
|
||||
"Either the agent is not available or there was an error getting it.",
|
||||
variant: "destructive",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setRunningAgent(true);
|
||||
|
||||
try {
|
||||
const libraryAgent = await resolveResponse(
|
||||
postV2AddMarketplaceAgent({
|
||||
store_listing_version_id: storeAgent?.store_listing_version_id || "",
|
||||
source: "onboarding",
|
||||
}),
|
||||
);
|
||||
|
||||
const { id: runID } = await api.executeGraph(
|
||||
libraryAgent.graph_id as GraphID,
|
||||
libraryAgent.graph_version,
|
||||
onboarding.state.agentInput || {},
|
||||
inputCredentials,
|
||||
"onboarding",
|
||||
);
|
||||
|
||||
onboarding.updateState({ onboardingAgentExecutionId: runID });
|
||||
|
||||
router.push("/onboarding/6-congrats");
|
||||
} catch (error) {
|
||||
console.error("Error running agent:", error);
|
||||
|
||||
toast({
|
||||
title: "Error running agent",
|
||||
description:
|
||||
"There was an error running your agent. Please try again or try choosing a different agent if it still fails.",
|
||||
variant: "destructive",
|
||||
});
|
||||
|
||||
setRunningAgent(false);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ready: agentGraphQueryIsSuccess && storeAgentQueryIsSuccess,
|
||||
error: agentGraphQueryError || storeAgentQueryError,
|
||||
agentGraph: agentGraphMeta || null,
|
||||
onboarding,
|
||||
showInput,
|
||||
storeAgent: storeAgent || null,
|
||||
runningAgent,
|
||||
credentialsValid,
|
||||
credentialsLoaded,
|
||||
handleSetAgentInput,
|
||||
handleRunAgent,
|
||||
handleNewRun,
|
||||
handleCredentialsChange: setInputCredentials,
|
||||
handleCredentialsValidationChange: setCredentialsValid,
|
||||
handleCredentialsLoadingChange: (v: boolean) => setCredentialsLoaded(!v),
|
||||
};
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
"use client";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useOnboarding } from "../../../../providers/onboarding/onboarding-provider";
|
||||
import { resolveResponse } from "@/app/api/helpers";
|
||||
import { getV1OnboardingState } from "@/app/api/__generated__/endpoints/onboarding/onboarding";
|
||||
import { postV2AddMarketplaceAgent } from "@/app/api/__generated__/endpoints/library/library";
|
||||
import { Confetti } from "@/components/molecules/Confetti/Confetti";
|
||||
import type { ConfettiRef } from "@/components/molecules/Confetti/Confetti";
|
||||
|
||||
export default function Page() {
|
||||
const { completeStep } = useOnboarding(7, "AGENT_INPUT");
|
||||
const router = useRouter();
|
||||
const api = useBackendAPI();
|
||||
const [showText, setShowText] = useState(false);
|
||||
const [showSubtext, setShowSubtext] = useState(false);
|
||||
const confettiRef = useRef<ConfettiRef>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Fire side cannons for a celebratory effect
|
||||
const duration = 1500;
|
||||
const end = Date.now() + duration;
|
||||
|
||||
function frame() {
|
||||
confettiRef.current?.fire({
|
||||
particleCount: 4,
|
||||
angle: 60,
|
||||
spread: 70,
|
||||
origin: { x: 0, y: 0.6 },
|
||||
shapes: ["square"],
|
||||
scalar: 0.8,
|
||||
gravity: 0.6,
|
||||
decay: 0.93,
|
||||
});
|
||||
confettiRef.current?.fire({
|
||||
particleCount: 4,
|
||||
angle: 120,
|
||||
spread: 70,
|
||||
origin: { x: 1, y: 0.6 },
|
||||
shapes: ["square"],
|
||||
scalar: 0.8,
|
||||
gravity: 0.6,
|
||||
decay: 0.93,
|
||||
});
|
||||
|
||||
if (Date.now() < end) {
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
}
|
||||
|
||||
frame();
|
||||
|
||||
const timer0 = setTimeout(() => {
|
||||
setShowText(true);
|
||||
}, 100);
|
||||
|
||||
const timer1 = setTimeout(() => {
|
||||
setShowSubtext(true);
|
||||
}, 500);
|
||||
|
||||
const timer2 = setTimeout(async () => {
|
||||
completeStep("CONGRATS");
|
||||
|
||||
try {
|
||||
const onboarding = await resolveResponse(getV1OnboardingState());
|
||||
if (onboarding?.selectedStoreListingVersionId) {
|
||||
try {
|
||||
const libraryAgent = await resolveResponse(
|
||||
postV2AddMarketplaceAgent({
|
||||
store_listing_version_id:
|
||||
onboarding.selectedStoreListingVersionId,
|
||||
source: "onboarding",
|
||||
}),
|
||||
);
|
||||
router.replace(`/library/agents/${libraryAgent.id}`);
|
||||
} catch (error) {
|
||||
console.error("Failed to add agent to library:", error);
|
||||
router.replace("/library");
|
||||
}
|
||||
} else {
|
||||
router.replace("/library");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to get onboarding data:", error);
|
||||
router.replace("/library");
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer0);
|
||||
clearTimeout(timer1);
|
||||
clearTimeout(timer2);
|
||||
};
|
||||
}, [completeStep, router, api]);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen flex-col items-center justify-center bg-violet-100">
|
||||
<Confetti ref={confettiRef} manualstart />
|
||||
<div
|
||||
className={cn(
|
||||
"z-10 -mb-16 text-9xl duration-500",
|
||||
showText ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
>
|
||||
🎉
|
||||
</div>
|
||||
<h1
|
||||
className={cn(
|
||||
"font-poppins text-9xl font-medium tracking-tighter text-violet-700 duration-500",
|
||||
showText ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
>
|
||||
Congrats!
|
||||
</h1>
|
||||
<p
|
||||
className={cn(
|
||||
"mb-16 mt-4 font-poppins text-2xl font-medium text-violet-800 transition-opacity duration-500",
|
||||
showSubtext ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
>
|
||||
You earned 3$ for running your first agent
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import StarRating from "./StarRating";
|
||||
import SmartImage from "@/components/__legacy__/SmartImage";
|
||||
import { StoreAgentDetails } from "@/app/api/__generated__/models/storeAgentDetails";
|
||||
|
||||
type OnboardingAgentCardProps = {
|
||||
agent?: StoreAgentDetails;
|
||||
selected?: boolean;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
export default function OnboardingAgentCard({
|
||||
agent,
|
||||
selected,
|
||||
onClick,
|
||||
}: OnboardingAgentCardProps) {
|
||||
if (!agent) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative animate-pulse",
|
||||
"h-[394px] w-[368px] rounded-[20px] border border-transparent bg-zinc-200",
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
agent_image,
|
||||
creator_avatar,
|
||||
agent_name,
|
||||
description,
|
||||
creator,
|
||||
runs,
|
||||
rating,
|
||||
} = agent;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative cursor-pointer transition-all duration-200 ease-in-out",
|
||||
"h-[394px] w-[368px] rounded-[20px] border border-transparent bg-white",
|
||||
selected ? "bg-[#F5F3FF80]" : "hover:border-zinc-400",
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{/* Image container */}
|
||||
<div className="relative">
|
||||
<SmartImage
|
||||
src={agent_image?.[0]}
|
||||
alt="Agent cover"
|
||||
className="m-2 h-[196px] w-[350px] rounded-[16px]"
|
||||
/>
|
||||
{/* Profile picture overlay */}
|
||||
<div className="absolute bottom-2 left-4">
|
||||
<SmartImage
|
||||
src={creator_avatar}
|
||||
alt="Profile picture"
|
||||
className="h-[50px] w-[50px] rounded-full border border-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content container */}
|
||||
<div className="flex h-[180px] flex-col justify-between px-4 pb-3">
|
||||
{/* Text content wrapper */}
|
||||
<div>
|
||||
{/* Title - 2 lines max */}
|
||||
<p className="data-sentry-unmask text-md line-clamp-2 max-h-[50px] font-sans text-base font-medium leading-normal text-zinc-800">
|
||||
{agent_name}
|
||||
</p>
|
||||
|
||||
{/* Author - single line with truncate */}
|
||||
<p className="data-sentry-unmask truncate text-sm font-normal leading-normal text-zinc-600">
|
||||
by {creator}
|
||||
</p>
|
||||
|
||||
{/* Description - 3 lines max */}
|
||||
<p
|
||||
className={cn(
|
||||
"mt-2 line-clamp-3 text-sm leading-5",
|
||||
selected ? "text-zinc-500" : "text-zinc-400",
|
||||
)}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Bottom stats */}
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span className="mt-1 font-sans text-sm font-medium text-zinc-800">
|
||||
{runs?.toLocaleString("en-US")} runs
|
||||
</span>
|
||||
<StarRating rating={rating} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 rounded-[20px] border-2 transition-all duration-200 ease-in-out",
|
||||
selected ? "border-violet-700" : "border-transparent",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { ChevronLeft } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
interface OnboardingBackButtonProps {
|
||||
href: string;
|
||||
}
|
||||
|
||||
export default function OnboardingBackButton({
|
||||
href,
|
||||
}: OnboardingBackButtonProps) {
|
||||
return (
|
||||
<Link
|
||||
className="flex items-center gap-2 font-sans text-base font-medium text-zinc-700 transition-colors duration-200 hover:text-zinc-800"
|
||||
href={href}
|
||||
>
|
||||
<ChevronLeft size={24} className="-mr-1" />
|
||||
<span>Back</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { LoadingSpinner } from "@/components/__legacy__/ui/loading";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
|
||||
const variants = {
|
||||
default: "bg-zinc-700 hover:bg-zinc-800",
|
||||
violet: "bg-violet-600 hover:bg-violet-700",
|
||||
};
|
||||
|
||||
type OnboardingButtonProps = {
|
||||
className?: string;
|
||||
variant?: keyof typeof variants;
|
||||
children?: React.ReactNode;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function OnboardingButton({
|
||||
className,
|
||||
variant = "default",
|
||||
children,
|
||||
loading,
|
||||
disabled,
|
||||
onClick,
|
||||
href,
|
||||
icon,
|
||||
}: OnboardingButtonProps) {
|
||||
const [internalLoading, setInternalLoading] = useState(false);
|
||||
const isLoading = loading !== undefined ? loading : internalLoading;
|
||||
|
||||
const buttonClasses = useMemo(
|
||||
() =>
|
||||
cn(
|
||||
"font-sans text-white text-sm font-medium",
|
||||
"inline-flex justify-center items-center",
|
||||
"h-12 min-w-[100px] rounded-full py-3 px-5",
|
||||
"transition-colors duration-200",
|
||||
className,
|
||||
disabled ? "bg-zinc-300 cursor-not-allowed" : variants[variant],
|
||||
),
|
||||
[disabled, variant, className],
|
||||
);
|
||||
|
||||
const onClickInternal = useCallback(() => {
|
||||
setInternalLoading(true);
|
||||
if (onClick) {
|
||||
onClick();
|
||||
}
|
||||
}, [setInternalLoading, onClick]);
|
||||
|
||||
if (href && !disabled) {
|
||||
return (
|
||||
<Link href={href} onClick={onClickInternal} className={buttonClasses}>
|
||||
{isLoading && <LoadingSpinner className="mr-2 size-5" />}
|
||||
{icon && !isLoading && <>{icon}</>}
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClickInternal}
|
||||
disabled={disabled}
|
||||
className={buttonClasses}
|
||||
>
|
||||
{isLoading && <LoadingSpinner className="mr-2 size-5" />}
|
||||
{icon && !isLoading && <>{icon}</>}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import SmartImage from "@/components/__legacy__/SmartImage";
|
||||
|
||||
type OnboardingGridElementProps = {
|
||||
name: string;
|
||||
text: string;
|
||||
icon: string;
|
||||
selected: boolean;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
function OnboardingGridElement({
|
||||
name,
|
||||
text,
|
||||
icon,
|
||||
selected,
|
||||
onClick,
|
||||
}: OnboardingGridElementProps) {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"relative flex h-[236px] w-[200px] flex-col items-start gap-2 rounded-xl border border-transparent bg-white p-[15px] font-sans",
|
||||
"transition-all duration-200 ease-in-out",
|
||||
selected ? "bg-[#F5F3FF80]" : "hover:border-zinc-400",
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<SmartImage
|
||||
src={icon}
|
||||
alt={`Logo of ${name}`}
|
||||
imageContain
|
||||
className="h-12 w-12 rounded-lg"
|
||||
/>
|
||||
<span className="text-md mt-4 w-full text-left font-medium leading-normal text-[#121212]">
|
||||
{name}
|
||||
</span>
|
||||
<span className="w-full text-left text-[11.5px] font-normal leading-5 text-zinc-500">
|
||||
{text}
|
||||
</span>
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 rounded-xl border-2 transition-all duration-200 ease-in-out",
|
||||
selected ? "border-violet-700" : "border-transparent",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
type OnboardingGridProps = {
|
||||
className?: string;
|
||||
elements: Array<{
|
||||
name: string;
|
||||
text: string;
|
||||
icon: string;
|
||||
}>;
|
||||
selected?: string[];
|
||||
onSelect: (name: string) => void;
|
||||
};
|
||||
|
||||
export function OnboardingGrid({
|
||||
className,
|
||||
elements,
|
||||
selected,
|
||||
onSelect,
|
||||
}: OnboardingGridProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
className,
|
||||
"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4",
|
||||
)}
|
||||
>
|
||||
{elements.map((element) => (
|
||||
<OnboardingGridElement
|
||||
key={element.name}
|
||||
name={element.name}
|
||||
text={element.text}
|
||||
icon={element.icon}
|
||||
selected={selected?.includes(element.name) || false}
|
||||
onClick={() => onSelect(element.name)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface OnboardingInputProps {
|
||||
className?: string;
|
||||
placeholder: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export default function OnboardingInput({
|
||||
className,
|
||||
placeholder,
|
||||
value,
|
||||
onChange,
|
||||
}: OnboardingInputProps) {
|
||||
return (
|
||||
<input
|
||||
className={cn(
|
||||
className,
|
||||
"font-poppin relative h-[50px] w-[512px] rounded-[25px] border border-transparent bg-white px-4 text-sm font-normal leading-normal text-zinc-900",
|
||||
"transition-all duration-200 ease-in-out placeholder:text-zinc-400",
|
||||
"focus:border-transparent focus:bg-[#F5F3FF80] focus:outline-none focus:ring-2 focus:ring-violet-700",
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Check } from "lucide-react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
type OnboardingListElementProps = {
|
||||
label: string;
|
||||
text: string;
|
||||
selected?: boolean;
|
||||
custom?: boolean;
|
||||
onClick: (content: string) => void;
|
||||
};
|
||||
|
||||
export function OnboardingListElement({
|
||||
label,
|
||||
text,
|
||||
selected,
|
||||
custom,
|
||||
onClick,
|
||||
}: OnboardingListElementProps) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [content, setContent] = useState(text);
|
||||
|
||||
useEffect(() => {
|
||||
if (selected && custom && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [selected, custom]);
|
||||
|
||||
const setCustomText = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setContent(e.target.value);
|
||||
onClick(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => onClick(content)}
|
||||
className={cn(
|
||||
"relative flex h-[78px] w-[530px] items-center rounded-xl border border-transparent px-5 py-4 transition-all duration-200 ease-in-out",
|
||||
selected ? "bg-[#F5F3FF80]" : "bg-white hover:border-zinc-400",
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full flex-col items-start gap-1">
|
||||
<span className="text-sm font-medium text-zinc-700">{label}</span>
|
||||
{custom && selected ? (
|
||||
<input
|
||||
ref={inputRef}
|
||||
className={cn(
|
||||
selected ? "text-zinc-600" : "text-zinc-400",
|
||||
"font-poppin w-full border-0 bg-[#F5F3FF80] text-sm focus:outline-none",
|
||||
)}
|
||||
placeholder="Please specify"
|
||||
value={content}
|
||||
onChange={setCustomText}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className={cn(
|
||||
selected ? "text-zinc-600" : "text-zinc-400",
|
||||
"text-sm",
|
||||
)}
|
||||
>
|
||||
{custom ? "Please specify" : text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!custom && (
|
||||
<div className="absolute right-4">
|
||||
<Check
|
||||
size={24}
|
||||
className={cn(
|
||||
"transition-all duration-200 ease-in-out",
|
||||
selected ? "text-violet-700" : "text-transparent",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 rounded-xl border-2 transition-all duration-200 ease-in-out",
|
||||
selected ? "border-violet-700" : "border-transparent",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
type OnboardingListProps = {
|
||||
className?: string;
|
||||
elements: Array<{
|
||||
label: string;
|
||||
text: string;
|
||||
id: string;
|
||||
}>;
|
||||
selectedId?: string | null;
|
||||
onSelect: (id: string) => void;
|
||||
};
|
||||
|
||||
function OnboardingList({
|
||||
className,
|
||||
elements,
|
||||
selectedId,
|
||||
onSelect,
|
||||
}: OnboardingListProps) {
|
||||
const isCustom = useCallback(() => {
|
||||
return (
|
||||
selectedId !== null &&
|
||||
!elements.some((element) => element.id === selectedId)
|
||||
);
|
||||
}, [selectedId, elements]);
|
||||
|
||||
return (
|
||||
<div className={cn(className, "flex flex-col gap-2")}>
|
||||
{elements.map((element) => (
|
||||
<OnboardingListElement
|
||||
key={element.id}
|
||||
label={element.label}
|
||||
text={element.text}
|
||||
selected={element.id === selectedId}
|
||||
onClick={() => onSelect(element.id)}
|
||||
/>
|
||||
))}
|
||||
<OnboardingListElement
|
||||
label="Other"
|
||||
text={isCustom() ? selectedId! : ""}
|
||||
selected={isCustom()}
|
||||
custom
|
||||
onClick={(c) => {
|
||||
onSelect(c);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default OnboardingList;
|
||||
@@ -1,45 +0,0 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
interface OnboardingProgressProps {
|
||||
totalSteps: number;
|
||||
toStep: number;
|
||||
}
|
||||
|
||||
export default function OnboardingProgress({
|
||||
totalSteps,
|
||||
toStep,
|
||||
}: OnboardingProgressProps) {
|
||||
const [animatedStep, setAnimatedStep] = useState(toStep - 1);
|
||||
const isInitialMount = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialMount.current) {
|
||||
// On initial mount, just set the position without animation
|
||||
isInitialMount.current = false;
|
||||
return;
|
||||
}
|
||||
// After initial mount, animate position changes
|
||||
setAnimatedStep(toStep - 1);
|
||||
}, [toStep]);
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center justify-center gap-3">
|
||||
{/* Background circles */}
|
||||
{Array.from({ length: totalSteps + 1 }).map((_, index) => (
|
||||
<div key={index} className="h-2 w-2 rounded-full bg-zinc-400" />
|
||||
))}
|
||||
|
||||
{/* Animated progress indicator */}
|
||||
<div
|
||||
className={`absolute left-0 h-2 w-7 rounded-full bg-zinc-400 ${
|
||||
!isInitialMount.current
|
||||
? "transition-all duration-300 ease-in-out"
|
||||
: ""
|
||||
}`}
|
||||
style={{
|
||||
transform: `translateX(${animatedStep * 20}px)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
"use client";
|
||||
import { ReactNode } from "react";
|
||||
import OnboardingBackButton from "./OnboardingBackButton";
|
||||
import { cn } from "@/lib/utils";
|
||||
import OnboardingProgress from "./OnboardingProgress";
|
||||
import { useOnboarding } from "../../../../providers/onboarding/onboarding-provider";
|
||||
|
||||
export function OnboardingStep({
|
||||
dotted,
|
||||
children,
|
||||
}: {
|
||||
dotted?: boolean;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative flex min-h-screen w-full flex-col">
|
||||
{dotted && (
|
||||
<div className="absolute left-1/2 h-full w-1/2 bg-white bg-[radial-gradient(#e5e7eb77_1px,transparent_1px)] [background-size:10px_10px]"></div>
|
||||
)}
|
||||
<div className="z-10 flex flex-col items-center">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface OnboardingHeaderProps {
|
||||
backHref: string;
|
||||
transparent?: boolean;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export function OnboardingHeader({
|
||||
backHref,
|
||||
transparent,
|
||||
children,
|
||||
}: OnboardingHeaderProps) {
|
||||
const { step } = useOnboarding();
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-10 w-full">
|
||||
<div
|
||||
className={cn(transparent ? "bg-transparent" : "bg-gray-100", "pb-5")}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between px-5 py-4">
|
||||
<OnboardingBackButton href={backHref} />
|
||||
<OnboardingProgress totalSteps={5} toStep={(step || 1) - 1} />
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{!transparent && (
|
||||
<div className="h-4 w-full bg-gradient-to-b from-gray-100 via-gray-100/50 to-transparent" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function OnboardingFooter({ children }: { children?: ReactNode }) {
|
||||
return (
|
||||
<div className="sticky bottom-0 z-10 w-full">
|
||||
<div className="h-4 w-full bg-gradient-to-t from-gray-100 via-gray-100/50 to-transparent" />
|
||||
<div className="flex justify-center bg-gray-100">
|
||||
<div className="px-5 py-5">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
const variants = {
|
||||
header: "font-poppin text-xl font-medium leading-7 text-zinc-900",
|
||||
subheader: "font-sans text-sm font-medium leading-6 text-zinc-800",
|
||||
default: "font-sans text-sm font-normal leading-6 text-zinc-500",
|
||||
};
|
||||
|
||||
export function OnboardingText({
|
||||
className,
|
||||
center,
|
||||
variant = "default",
|
||||
children,
|
||||
}: {
|
||||
className?: string;
|
||||
center?: boolean;
|
||||
variant?: keyof typeof variants;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"w-full",
|
||||
center ? "text-center" : "text-left",
|
||||
variants[variant] || variants.default,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
interface Props {
|
||||
currentStep: number;
|
||||
totalSteps: number;
|
||||
}
|
||||
|
||||
export function ProgressBar({ currentStep, totalSteps }: Props) {
|
||||
const percent = (currentStep / totalSteps) * 100;
|
||||
|
||||
return (
|
||||
<div className="absolute left-0 top-0 h-[0.625rem] w-full bg-neutral-300">
|
||||
<div
|
||||
className="h-full bg-purple-400 shadow-[0_0_4px_2px_rgba(168,85,247,0.5)] transition-all duration-500 ease-out"
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
selected: boolean;
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SelectableCard({
|
||||
icon,
|
||||
label,
|
||||
selected,
|
||||
onClick,
|
||||
className,
|
||||
}: Props) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-pressed={selected}
|
||||
className={cn(
|
||||
"flex h-[9rem] w-[10.375rem] shrink-0 flex-col items-center justify-center gap-3 rounded-xl border-2 bg-white px-6 py-5 transition-all hover:shadow-sm md:shrink lg:gap-2 lg:px-10 lg:py-8",
|
||||
className,
|
||||
selected
|
||||
? "border-purple-500 bg-purple-50 shadow-sm"
|
||||
: "border-transparent",
|
||||
)}
|
||||
>
|
||||
<Text
|
||||
variant="lead"
|
||||
as="span"
|
||||
className={selected ? "text-neutral-900" : "text-purple-600"}
|
||||
>
|
||||
{icon}
|
||||
</Text>
|
||||
<Text variant="body-medium" as="span" className="whitespace-nowrap">
|
||||
{label}
|
||||
</Text>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useMemo } from "react";
|
||||
import { FaRegStar, FaStar, FaStarHalfAlt } from "react-icons/fa";
|
||||
|
||||
export default function StarRating({
|
||||
className,
|
||||
starSize,
|
||||
rating,
|
||||
}: {
|
||||
className?: string;
|
||||
starSize?: number;
|
||||
rating: number;
|
||||
}) {
|
||||
// Round to 1 decimal place
|
||||
const roundedRating = Math.round(rating * 10) / 10;
|
||||
starSize ??= 15;
|
||||
|
||||
// Generate array of 5 star values
|
||||
const stars = useMemo(
|
||||
() =>
|
||||
Array(5)
|
||||
.fill(0)
|
||||
.map((_, index) => {
|
||||
const difference = roundedRating - index;
|
||||
|
||||
if (difference >= 1) {
|
||||
return "full";
|
||||
} else if (difference > 0) {
|
||||
// Half star for values between 0.2 and 0.8
|
||||
return difference >= 0.8
|
||||
? "full"
|
||||
: difference >= 0.2
|
||||
? "half"
|
||||
: "empty";
|
||||
}
|
||||
return "empty";
|
||||
}),
|
||||
[roundedRating],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-0.5 text-sm font-medium text-zinc-800",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{/* Display numerical rating */}
|
||||
<span className="mr-1 mt-0.5">{roundedRating}</span>
|
||||
|
||||
{/* Display stars */}
|
||||
{stars.map((starType, index) => {
|
||||
if (starType === "full") {
|
||||
return <FaStar size={starSize} key={index} />;
|
||||
} else if (starType === "half") {
|
||||
return <FaStarHalfAlt size={starSize} key={index} />;
|
||||
} else {
|
||||
return <FaRegStar size={starSize} key={index} />;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
totalSteps: number;
|
||||
currentStep: number;
|
||||
}
|
||||
|
||||
export function StepIndicator({ totalSteps, currentStep }: Props) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{Array.from({ length: totalSteps }, (_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn(
|
||||
"h-2 rounded-full transition-all",
|
||||
i + 1 === currentStep ? "w-6 bg-foreground" : "w-2 bg-gray-300",
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,8 +6,8 @@ export default function OnboardingLayout({
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex min-h-screen w-full items-center justify-center bg-gray-100">
|
||||
<main className="mx-auto flex w-full flex-col items-center">
|
||||
<div className="relative flex min-h-screen w-full flex-col bg-gray-100">
|
||||
<main className="flex w-full flex-1 flex-col items-center justify-center">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,73 +1,59 @@
|
||||
"use client";
|
||||
import { getV1OnboardingState } from "@/app/api/__generated__/endpoints/onboarding/onboarding";
|
||||
import { getOnboardingStatus, resolveResponse } from "@/app/api/helpers";
|
||||
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { CaretLeft } from "@phosphor-icons/react";
|
||||
import { ProgressBar } from "./components/ProgressBar";
|
||||
import { StepIndicator } from "./components/StepIndicator";
|
||||
import { PainPointsStep } from "./steps/PainPointsStep";
|
||||
import { PreparingStep } from "./steps/PreparingStep";
|
||||
import { RoleStep } from "./steps/RoleStep";
|
||||
import { WelcomeStep } from "./steps/WelcomeStep";
|
||||
import { useOnboardingWizardStore } from "./store";
|
||||
import { useOnboardingPage } from "./useOnboardingPage";
|
||||
|
||||
export default function OnboardingPage() {
|
||||
const router = useRouter();
|
||||
const { currentStep, isLoading, handlePreparingComplete } =
|
||||
useOnboardingPage();
|
||||
const prevStep = useOnboardingWizardStore((s) => s.prevStep);
|
||||
|
||||
useEffect(() => {
|
||||
async function redirectToStep() {
|
||||
try {
|
||||
// Check if onboarding is enabled (also gets chat flag for redirect)
|
||||
const { shouldShowOnboarding } = await getOnboardingStatus();
|
||||
if (isLoading) return null;
|
||||
|
||||
if (!shouldShowOnboarding) {
|
||||
router.replace("/");
|
||||
return;
|
||||
}
|
||||
const totalSteps = 4;
|
||||
const showDots = currentStep <= 3;
|
||||
const showBack = currentStep > 1 && currentStep <= 3;
|
||||
|
||||
const onboarding = await resolveResponse(getV1OnboardingState());
|
||||
const showProgressBar = currentStep <= 3;
|
||||
|
||||
// Handle completed onboarding
|
||||
if (onboarding.completedSteps.includes("GET_RESULTS")) {
|
||||
router.replace("/");
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<div className="flex min-h-screen w-full flex-col items-center">
|
||||
{showProgressBar && (
|
||||
<ProgressBar currentStep={currentStep} totalSteps={totalSteps} />
|
||||
)}
|
||||
|
||||
// Redirect to appropriate step based on completed steps
|
||||
if (onboarding.completedSteps.includes("AGENT_INPUT")) {
|
||||
router.push("/onboarding/5-run");
|
||||
return;
|
||||
}
|
||||
{showBack && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={prevStep}
|
||||
className="text-md absolute left-6 top-6 flex items-center gap-1 text-zinc-500 transition-colors duration-200 hover:text-zinc-900"
|
||||
>
|
||||
<CaretLeft size={16} />
|
||||
Back
|
||||
</button>
|
||||
)}
|
||||
|
||||
if (onboarding.completedSteps.includes("AGENT_NEW_RUN")) {
|
||||
router.push("/onboarding/5-run");
|
||||
return;
|
||||
}
|
||||
<div className="flex flex-1 items-center py-16">
|
||||
{currentStep === 1 && <WelcomeStep />}
|
||||
{currentStep === 2 && <RoleStep />}
|
||||
{currentStep === 3 && <PainPointsStep />}
|
||||
{currentStep === 4 && (
|
||||
<PreparingStep onComplete={handlePreparingComplete} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
if (onboarding.completedSteps.includes("AGENT_CHOICE")) {
|
||||
router.push("/onboarding/5-run");
|
||||
return;
|
||||
}
|
||||
|
||||
if (onboarding.completedSteps.includes("INTEGRATIONS")) {
|
||||
router.push("/onboarding/4-agent");
|
||||
return;
|
||||
}
|
||||
|
||||
if (onboarding.completedSteps.includes("USAGE_REASON")) {
|
||||
router.push("/onboarding/3-services");
|
||||
return;
|
||||
}
|
||||
|
||||
if (onboarding.completedSteps.includes("WELCOME")) {
|
||||
router.push("/onboarding/2-reason");
|
||||
return;
|
||||
}
|
||||
|
||||
// Default: redirect to first step
|
||||
router.push("/onboarding/1-welcome");
|
||||
} catch (error) {
|
||||
console.error("Failed to determine onboarding step:", error);
|
||||
router.replace("/");
|
||||
}
|
||||
}
|
||||
|
||||
redirectToStep();
|
||||
}, [router]);
|
||||
|
||||
return <LoadingSpinner size="large" cover />;
|
||||
{showDots && (
|
||||
<div className="pb-8">
|
||||
<StepIndicator totalSteps={3} currentStep={currentStep} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
"use client";
|
||||
import { postV1ResetOnboardingProgress } from "@/app/api/__generated__/endpoints/onboarding/onboarding";
|
||||
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function OnboardingResetPage() {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
postV1ResetOnboardingProgress()
|
||||
.then(() => {
|
||||
toast({
|
||||
title: "Onboarding reset successfully",
|
||||
description: "You can now start the onboarding process again",
|
||||
variant: "success",
|
||||
});
|
||||
|
||||
router.push("/onboarding");
|
||||
})
|
||||
.catch(() => {
|
||||
toast({
|
||||
title: "Failed to reset onboarding",
|
||||
description: "Please try again later",
|
||||
variant: "destructive",
|
||||
});
|
||||
});
|
||||
}, [toast, router]);
|
||||
|
||||
return <LoadingSpinner cover />;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import { FadeIn } from "@/components/atoms/FadeIn/FadeIn";
|
||||
import { SelectableCard } from "../components/SelectableCard";
|
||||
import { usePainPointsStep } from "./usePainPointsStep";
|
||||
import { Emoji } from "@/components/atoms/Emoji/Emoji";
|
||||
|
||||
const ALL_PAIN_POINTS: { id: string; label: string; icon: ReactNode }[] = [
|
||||
{
|
||||
id: "Finding leads",
|
||||
label: "Finding leads",
|
||||
icon: <Emoji text="🔍" size={32} />,
|
||||
},
|
||||
{
|
||||
id: "Email & outreach",
|
||||
label: "Email & outreach",
|
||||
icon: <Emoji text="📧" size={32} />,
|
||||
},
|
||||
{
|
||||
id: "Reports & data",
|
||||
label: "Reports & data",
|
||||
icon: <Emoji text="📊" size={32} />,
|
||||
},
|
||||
{
|
||||
id: "Customer support",
|
||||
label: "Customer support",
|
||||
icon: <Emoji text="💬" size={32} />,
|
||||
},
|
||||
{
|
||||
id: "Social media",
|
||||
label: "Social media",
|
||||
icon: <Emoji text="📱" size={32} />,
|
||||
},
|
||||
{
|
||||
id: "CRM & data entry",
|
||||
label: "CRM & data entry",
|
||||
icon: <Emoji text="📝" size={32} />,
|
||||
},
|
||||
{
|
||||
id: "Scheduling",
|
||||
label: "Scheduling",
|
||||
icon: <Emoji text="🗓️" size={32} />,
|
||||
},
|
||||
{ id: "Research", label: "Research", icon: <Emoji text="🔬" size={32} /> },
|
||||
{
|
||||
id: "Something else",
|
||||
label: "Something else",
|
||||
icon: <Emoji text="🚩" size={32} />,
|
||||
},
|
||||
];
|
||||
|
||||
function orderPainPoints(topIDs: string[]) {
|
||||
const top = topIDs
|
||||
.map((id) => ALL_PAIN_POINTS.find((p) => p.id === id))
|
||||
.filter((p): p is (typeof ALL_PAIN_POINTS)[number] => p != null);
|
||||
const rest = ALL_PAIN_POINTS.filter(
|
||||
(p) => !topIDs.includes(p.id) && p.id !== "Something else",
|
||||
);
|
||||
const somethingElse = ALL_PAIN_POINTS.find((p) => p.id === "Something else")!;
|
||||
return [...top, ...rest, somethingElse];
|
||||
}
|
||||
|
||||
export function PainPointsStep() {
|
||||
const {
|
||||
topIDs,
|
||||
painPoints,
|
||||
otherPainPoint,
|
||||
togglePainPoint,
|
||||
setOtherPainPoint,
|
||||
hasSomethingElse,
|
||||
canContinue,
|
||||
handleLaunch,
|
||||
} = usePainPointsStep();
|
||||
|
||||
const orderedPainPoints = orderPainPoints(topIDs);
|
||||
|
||||
return (
|
||||
<FadeIn>
|
||||
<div className="flex w-full flex-col items-center gap-12 px-4">
|
||||
<div className="flex max-w-lg flex-col items-center gap-2 px-4 text-center">
|
||||
<Text
|
||||
variant="h3"
|
||||
className="!text-[1.5rem] !leading-[2rem] md:!text-[1.75rem] md:!leading-[2.5rem]"
|
||||
>
|
||||
What's eating your time?
|
||||
</Text>
|
||||
<Text variant="lead" className="!text-zinc-500">
|
||||
Pick the tasks you'd love to hand off to Autopilot
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col items-center gap-4">
|
||||
<div className="flex w-full max-w-[100vw] flex-nowrap gap-4 overflow-x-auto px-8 scrollbar-none md:grid md:grid-cols-3 md:overflow-hidden md:px-0">
|
||||
{orderedPainPoints.map((p) => (
|
||||
<SelectableCard
|
||||
key={p.id}
|
||||
icon={p.icon}
|
||||
label={p.label}
|
||||
selected={painPoints.includes(p.id)}
|
||||
onClick={() => togglePainPoint(p.id)}
|
||||
className="p-8"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{!hasSomethingElse ? (
|
||||
<Text variant="small" className="!text-zinc-500">
|
||||
Pick as many as you want — you can always change later
|
||||
</Text>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{hasSomethingElse && (
|
||||
<div className="-mb-5 w-full px-8 md:px-0">
|
||||
<Input
|
||||
id="other-pain-point"
|
||||
label="Other pain point"
|
||||
hideLabel
|
||||
placeholder="What else takes up your time?"
|
||||
value={otherPainPoint}
|
||||
onChange={(e) => setOtherPainPoint(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleLaunch}
|
||||
disabled={!canContinue}
|
||||
className="w-full max-w-xs"
|
||||
>
|
||||
Launch Autopilot
|
||||
</Button>
|
||||
</div>
|
||||
</FadeIn>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
import { AutoGPTLogo } from "@/components/atoms/AutoGPTLogo/AutoGPTLogo";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { TypingText } from "@/components/molecules/TypingText/TypingText";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Check } from "@phosphor-icons/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
const CHECKLIST = [
|
||||
"Personalizing your experience",
|
||||
"Connecting automation engines",
|
||||
"Building your space",
|
||||
] as const;
|
||||
|
||||
const STEP_DURATION_MS = 4000;
|
||||
const STEP_INTERVAL = STEP_DURATION_MS / CHECKLIST.length;
|
||||
|
||||
interface Props {
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
export function PreparingStep({ onComplete }: Props) {
|
||||
const [started, setStarted] = useState(false);
|
||||
const [completedItems, setCompletedItems] = useState(0);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const onCompleteRef = useRef(onComplete);
|
||||
onCompleteRef.current = onComplete;
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setStarted(true), 300);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!started) return;
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
const progressInterval = setInterval(() => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const pct = Math.min(100, (elapsed / STEP_DURATION_MS) * 100);
|
||||
setProgress(pct);
|
||||
|
||||
const items = Math.min(
|
||||
CHECKLIST.length,
|
||||
Math.floor(elapsed / STEP_INTERVAL) + 1,
|
||||
);
|
||||
setCompletedItems(items);
|
||||
|
||||
if (elapsed >= STEP_DURATION_MS) {
|
||||
clearInterval(progressInterval);
|
||||
onCompleteRef.current();
|
||||
}
|
||||
}, 50);
|
||||
|
||||
return () => clearInterval(progressInterval);
|
||||
}, [started]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full max-w-md flex-col items-center gap-8 px-4">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<AutoGPTLogo
|
||||
className="relative right-[3rem] h-24 w-[12rem]"
|
||||
hideText
|
||||
/>
|
||||
<Text variant="h3" className="text-center">
|
||||
<TypingText
|
||||
text="Preparing your workspace..."
|
||||
active={started}
|
||||
delay={400}
|
||||
speed={60}
|
||||
/>
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="h-2 w-full overflow-hidden rounded-full bg-gray-200">
|
||||
<div
|
||||
className="h-full rounded-full bg-purple-500 transition-all duration-100 ease-linear"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ul className="flex flex-col gap-3">
|
||||
{CHECKLIST.map((item, i) => (
|
||||
<li key={item} className="flex items-center gap-3">
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-6 w-6 items-center justify-center rounded-full transition-colors",
|
||||
i < completedItems
|
||||
? "bg-neutral-900 text-white"
|
||||
: "bg-gray-200 text-gray-400",
|
||||
)}
|
||||
>
|
||||
<Check size={14} weight="bold" />
|
||||
</div>
|
||||
<Text
|
||||
variant="body"
|
||||
as="span"
|
||||
className={cn(
|
||||
"transition-colors",
|
||||
i < completedItems ? "!text-black" : "!text-zinc-500",
|
||||
)}
|
||||
>
|
||||
{item}
|
||||
</Text>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
|
||||
import { FadeIn } from "@/components/atoms/FadeIn/FadeIn";
|
||||
import { SelectableCard } from "../components/SelectableCard";
|
||||
import { useOnboardingWizardStore } from "../store";
|
||||
import { Emoji } from "@/components/atoms/Emoji/Emoji";
|
||||
|
||||
const IMG_SIZE = 42;
|
||||
|
||||
const ROLES = [
|
||||
{
|
||||
id: "Founder/CEO",
|
||||
label: "Founder / CEO",
|
||||
icon: <Emoji text="🎯" size={IMG_SIZE} />,
|
||||
},
|
||||
{
|
||||
id: "Operations",
|
||||
label: "Operations",
|
||||
icon: <Emoji text="⚙️" size={IMG_SIZE} />,
|
||||
},
|
||||
{
|
||||
id: "Sales/BD",
|
||||
label: "Sales / BD",
|
||||
icon: <Emoji text="📈" size={IMG_SIZE} />,
|
||||
},
|
||||
{
|
||||
id: "Marketing",
|
||||
label: "Marketing",
|
||||
icon: <Emoji text="📢" size={IMG_SIZE} />,
|
||||
},
|
||||
{
|
||||
id: "Product/PM",
|
||||
label: "Product / PM",
|
||||
icon: <Emoji text="🔨" size={IMG_SIZE} />,
|
||||
},
|
||||
{
|
||||
id: "Engineering",
|
||||
label: "Engineering",
|
||||
icon: <Emoji text="💻" size={IMG_SIZE} />,
|
||||
},
|
||||
{
|
||||
id: "HR/People",
|
||||
label: "HR / People",
|
||||
icon: <Emoji text="👤" size={IMG_SIZE} />,
|
||||
},
|
||||
{ id: "Other", label: "Other", icon: <Emoji text="🚩" size={IMG_SIZE} /> },
|
||||
] as const;
|
||||
|
||||
export function RoleStep() {
|
||||
const name = useOnboardingWizardStore((s) => s.name);
|
||||
const role = useOnboardingWizardStore((s) => s.role);
|
||||
const otherRole = useOnboardingWizardStore((s) => s.otherRole);
|
||||
const setRole = useOnboardingWizardStore((s) => s.setRole);
|
||||
const setOtherRole = useOnboardingWizardStore((s) => s.setOtherRole);
|
||||
const nextStep = useOnboardingWizardStore((s) => s.nextStep);
|
||||
|
||||
const isOther = role === "Other";
|
||||
const canContinue = role && (!isOther || otherRole.trim());
|
||||
|
||||
function handleContinue() {
|
||||
if (canContinue) {
|
||||
nextStep();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FadeIn>
|
||||
<div className="flex w-full flex-col items-center gap-12 px-4">
|
||||
<div className="mx-auto flex w-full max-w-lg flex-col items-center gap-2 px-4 text-center">
|
||||
<Text
|
||||
variant="h3"
|
||||
className="!text-[1.5rem] !leading-[2rem] md:!text-[1.75rem] md:!leading-[2.5rem]"
|
||||
>
|
||||
What best describes you, {name}?
|
||||
</Text>
|
||||
<Text variant="lead" className="!text-zinc-500">
|
||||
Autopilot will tailor automations to your world
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full max-w-[100vw] flex-nowrap gap-4 overflow-x-auto px-8 scrollbar-none md:grid md:grid-cols-4 md:overflow-hidden md:px-0">
|
||||
{ROLES.map((r) => (
|
||||
<SelectableCard
|
||||
key={r.id}
|
||||
icon={r.icon}
|
||||
label={r.label}
|
||||
selected={role === r.id}
|
||||
onClick={() => setRole(r.id)}
|
||||
className="p-8"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{isOther && (
|
||||
<div className="-mb-5 w-full px-8 md:px-0">
|
||||
<Input
|
||||
id="other-role"
|
||||
label="Other role"
|
||||
hideLabel
|
||||
placeholder="Describe your role..."
|
||||
value={otherRole}
|
||||
onChange={(e) => setOtherRole(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleContinue}
|
||||
disabled={!canContinue}
|
||||
className="w-full max-w-xs"
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</FadeIn>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import { AutoGPTLogo } from "@/components/atoms/AutoGPTLogo/AutoGPTLogo";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/atoms/Tooltip/BaseTooltip";
|
||||
import { Question } from "@phosphor-icons/react";
|
||||
import { FadeIn } from "@/components/atoms/FadeIn/FadeIn";
|
||||
import { useOnboardingWizardStore } from "../store";
|
||||
|
||||
export function WelcomeStep() {
|
||||
const name = useOnboardingWizardStore((s) => s.name);
|
||||
const setName = useOnboardingWizardStore((s) => s.setName);
|
||||
const nextStep = useOnboardingWizardStore((s) => s.nextStep);
|
||||
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (name.trim()) {
|
||||
nextStep();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FadeIn>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex w-full max-w-lg flex-col items-center gap-4 px-4 md:gap-8"
|
||||
>
|
||||
<div className="mb-8 flex flex-col items-center gap-3 text-center md:mb-0">
|
||||
<AutoGPTLogo
|
||||
className="relative right-[3rem] h-24 w-[12rem]"
|
||||
hideText
|
||||
/>
|
||||
<Text variant="h3">Welcome to AutoGPT</Text>
|
||||
<Text variant="lead" as="span" className="!text-zinc-500">
|
||||
Let's personalize your experience so{" "}
|
||||
<span className="relative mr-3 inline-block bg-gradient-to-r from-purple-500 to-indigo-500 bg-clip-text text-transparent">
|
||||
Autopilot
|
||||
<span className="absolute -right-4 top-0">
|
||||
<TooltipProvider delayDuration={400}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="What is Autopilot?"
|
||||
className="inline-flex text-purple-500"
|
||||
>
|
||||
<Question size={14} />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Autopilot is AutoGPT's AI assistant that watches your
|
||||
connected apps, spots repetitive tasks you do every day
|
||||
and runs them for you automatically.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</span>
|
||||
</span>{" "}
|
||||
can start saving you time right away
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
id="first-name"
|
||||
label="Your first name"
|
||||
placeholder="e.g. John"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!name.trim()}
|
||||
className="w-full max-w-xs"
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</form>
|
||||
</FadeIn>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useOnboardingWizardStore } from "../store";
|
||||
|
||||
const ROLE_TOP_PICKS: Record<string, string[]> = {
|
||||
"Founder/CEO": [
|
||||
"Finding leads",
|
||||
"Reports & data",
|
||||
"Email & outreach",
|
||||
"Scheduling",
|
||||
],
|
||||
Operations: ["CRM & data entry", "Scheduling", "Reports & data"],
|
||||
"Sales/BD": ["Finding leads", "Email & outreach", "CRM & data entry"],
|
||||
Marketing: ["Social media", "Email & outreach", "Research"],
|
||||
"Product/PM": ["Research", "Reports & data", "Scheduling"],
|
||||
Engineering: ["Research", "Reports & data", "CRM & data entry"],
|
||||
"HR/People": ["Scheduling", "Email & outreach", "CRM & data entry"],
|
||||
};
|
||||
|
||||
export function getTopPickIDs(role: string) {
|
||||
return ROLE_TOP_PICKS[role] ?? [];
|
||||
}
|
||||
|
||||
export function usePainPointsStep() {
|
||||
const role = useOnboardingWizardStore((s) => s.role);
|
||||
const painPoints = useOnboardingWizardStore((s) => s.painPoints);
|
||||
const otherPainPoint = useOnboardingWizardStore((s) => s.otherPainPoint);
|
||||
const togglePainPoint = useOnboardingWizardStore((s) => s.togglePainPoint);
|
||||
const setOtherPainPoint = useOnboardingWizardStore(
|
||||
(s) => s.setOtherPainPoint,
|
||||
);
|
||||
const nextStep = useOnboardingWizardStore((s) => s.nextStep);
|
||||
|
||||
const topIDs = getTopPickIDs(role);
|
||||
const hasSomethingElse = painPoints.includes("Something else");
|
||||
const canContinue =
|
||||
painPoints.length > 0 &&
|
||||
(!hasSomethingElse || Boolean(otherPainPoint.trim()));
|
||||
|
||||
function handleLaunch() {
|
||||
if (canContinue) {
|
||||
nextStep();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
topIDs,
|
||||
painPoints,
|
||||
otherPainPoint,
|
||||
togglePainPoint,
|
||||
setOtherPainPoint,
|
||||
hasSomethingElse,
|
||||
canContinue,
|
||||
handleLaunch,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
export type Step = 1 | 2 | 3 | 4;
|
||||
|
||||
interface OnboardingWizardState {
|
||||
currentStep: Step;
|
||||
name: string;
|
||||
role: string;
|
||||
otherRole: string;
|
||||
painPoints: string[];
|
||||
otherPainPoint: string;
|
||||
setName(name: string): void;
|
||||
setRole(role: string): void;
|
||||
setOtherRole(otherRole: string): void;
|
||||
togglePainPoint(painPoint: string): void;
|
||||
setOtherPainPoint(otherPainPoint: string): void;
|
||||
nextStep(): void;
|
||||
prevStep(): void;
|
||||
goToStep(step: Step): void;
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
export const useOnboardingWizardStore = create<OnboardingWizardState>(
|
||||
(set) => ({
|
||||
currentStep: 1,
|
||||
name: "",
|
||||
role: "",
|
||||
otherRole: "",
|
||||
painPoints: [],
|
||||
otherPainPoint: "",
|
||||
setName(name) {
|
||||
set({ name });
|
||||
},
|
||||
setRole(role) {
|
||||
set({ role });
|
||||
},
|
||||
setOtherRole(otherRole) {
|
||||
set({ otherRole });
|
||||
},
|
||||
togglePainPoint(painPoint) {
|
||||
set((state) => {
|
||||
const exists = state.painPoints.includes(painPoint);
|
||||
return {
|
||||
painPoints: exists
|
||||
? state.painPoints.filter((p) => p !== painPoint)
|
||||
: [...state.painPoints, painPoint],
|
||||
};
|
||||
});
|
||||
},
|
||||
setOtherPainPoint(otherPainPoint) {
|
||||
set({ otherPainPoint });
|
||||
},
|
||||
nextStep() {
|
||||
set((state) => ({
|
||||
currentStep: Math.min(4, state.currentStep + 1) as Step,
|
||||
}));
|
||||
},
|
||||
prevStep() {
|
||||
set((state) => ({
|
||||
currentStep: Math.max(1, state.currentStep - 1) as Step,
|
||||
}));
|
||||
},
|
||||
goToStep(step) {
|
||||
set({ currentStep: step });
|
||||
},
|
||||
reset() {
|
||||
set({
|
||||
currentStep: 1,
|
||||
name: "",
|
||||
role: "",
|
||||
otherRole: "",
|
||||
painPoints: [],
|
||||
otherPainPoint: "",
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,109 @@
|
||||
import {
|
||||
getV1OnboardingState,
|
||||
postV1CompleteOnboardingStep,
|
||||
postV1SubmitOnboardingProfile,
|
||||
} from "@/app/api/__generated__/endpoints/onboarding/onboarding";
|
||||
import { resolveResponse } from "@/app/api/helpers";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Step, useOnboardingWizardStore } from "./store";
|
||||
|
||||
function parseStep(value: string | null): Step {
|
||||
const n = Number(value);
|
||||
if (n >= 1 && n <= 4) return n as Step;
|
||||
return 1;
|
||||
}
|
||||
|
||||
export function useOnboardingPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { isLoggedIn } = useSupabase();
|
||||
const currentStep = useOnboardingWizardStore((s) => s.currentStep);
|
||||
const goToStep = useOnboardingWizardStore((s) => s.goToStep);
|
||||
const reset = useOnboardingWizardStore((s) => s.reset);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const hasSubmitted = useRef(false);
|
||||
const hasInitialized = useRef(false);
|
||||
|
||||
// Initialise store from URL on mount, reset form data
|
||||
useEffect(() => {
|
||||
if (hasInitialized.current) return;
|
||||
hasInitialized.current = true;
|
||||
const urlStep = parseStep(searchParams.get("step"));
|
||||
reset();
|
||||
goToStep(urlStep);
|
||||
}, [searchParams, reset, goToStep]);
|
||||
|
||||
// Sync store → URL when step changes
|
||||
useEffect(() => {
|
||||
const urlStep = parseStep(searchParams.get("step"));
|
||||
if (currentStep !== urlStep) {
|
||||
router.replace(`/onboarding?step=${currentStep}`, { scroll: false });
|
||||
}
|
||||
}, [currentStep, router, searchParams]);
|
||||
|
||||
// Check if onboarding already completed
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) return;
|
||||
|
||||
async function checkCompletion() {
|
||||
try {
|
||||
const onboarding = await resolveResponse(getV1OnboardingState());
|
||||
if (onboarding.completedSteps.includes("VISIT_COPILOT")) {
|
||||
router.replace("/copilot");
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// If we can't check, show onboarding anyway
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
checkCompletion();
|
||||
}, [isLoggedIn, router]);
|
||||
|
||||
// Submit profile when entering step 4
|
||||
useEffect(() => {
|
||||
if (currentStep !== 4 || hasSubmitted.current) return;
|
||||
hasSubmitted.current = true;
|
||||
|
||||
const { name, role, otherRole, painPoints, otherPainPoint } =
|
||||
useOnboardingWizardStore.getState();
|
||||
const resolvedRole = role === "Other" ? otherRole : role;
|
||||
const resolvedPainPoints = painPoints
|
||||
.filter((p) => p !== "Something else")
|
||||
.concat(
|
||||
painPoints.includes("Something else") && otherPainPoint.trim()
|
||||
? [otherPainPoint.trim()]
|
||||
: [],
|
||||
);
|
||||
|
||||
postV1SubmitOnboardingProfile({
|
||||
user_name: name,
|
||||
user_role: resolvedRole,
|
||||
pain_points: resolvedPainPoints,
|
||||
}).catch(() => {
|
||||
// Best effort — profile data is non-critical for accessing copilot
|
||||
});
|
||||
}, [currentStep]);
|
||||
|
||||
async function handlePreparingComplete() {
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
await postV1CompleteOnboardingStep({ step: "VISIT_COPILOT" });
|
||||
router.replace("/copilot");
|
||||
return;
|
||||
} catch {
|
||||
if (attempt < 2) await new Promise((r) => setTimeout(r, 1000));
|
||||
}
|
||||
}
|
||||
router.replace("/copilot");
|
||||
}
|
||||
|
||||
return {
|
||||
currentStep,
|
||||
isLoading,
|
||||
handlePreparingComplete,
|
||||
};
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export async function GET(request: Request) {
|
||||
const { searchParams, origin } = new URL(request.url);
|
||||
const code = searchParams.get("code");
|
||||
|
||||
let next = "/";
|
||||
let next = "/copilot";
|
||||
|
||||
if (code) {
|
||||
const supabase = await getServerSupabase();
|
||||
@@ -25,15 +25,9 @@ export async function GET(request: Request) {
|
||||
const api = new BackendAPI();
|
||||
await api.createUser();
|
||||
|
||||
// Get onboarding status from backend (includes chat flag evaluated for this user)
|
||||
const { shouldShowOnboarding } = await getOnboardingStatus();
|
||||
if (shouldShowOnboarding) {
|
||||
next = "/onboarding";
|
||||
revalidatePath("/onboarding", "layout");
|
||||
} else {
|
||||
next = "/";
|
||||
revalidatePath(next, "layout");
|
||||
}
|
||||
next = shouldShowOnboarding ? "/onboarding" : "/copilot";
|
||||
revalidatePath(next, "layout");
|
||||
} catch (createUserError) {
|
||||
console.error("Error creating user:", createUserError);
|
||||
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { FeatureFlagPage } from "@/services/feature-flags/FeatureFlagPage";
|
||||
import { Flag } from "@/services/feature-flags/use-get-flag";
|
||||
import { CopilotPage } from "./CopilotPage";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<FeatureFlagPage flag={Flag.CHAT} whenDisabled="/library">
|
||||
<CopilotPage />
|
||||
</FeatureFlagPage>
|
||||
);
|
||||
return <CopilotPage />;
|
||||
}
|
||||
|
||||
@@ -36,13 +36,11 @@ export async function login(email: string, password: string) {
|
||||
const api = new BackendAPI();
|
||||
await api.createUser();
|
||||
|
||||
// Get onboarding status from backend (includes chat flag evaluated for this user)
|
||||
const { shouldShowOnboarding } = await getOnboardingStatus();
|
||||
const next = shouldShowOnboarding ? "/onboarding" : "/";
|
||||
|
||||
return {
|
||||
success: true,
|
||||
next,
|
||||
next: shouldShowOnboarding ? "/onboarding" : "/copilot",
|
||||
};
|
||||
} catch (err) {
|
||||
Sentry.captureException(err);
|
||||
|
||||
@@ -112,6 +112,7 @@ export function useLoginPage() {
|
||||
: "Unexpected error during login",
|
||||
variant: "destructive",
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
setIsLoggingIn(false);
|
||||
}
|
||||
|
||||
@@ -69,11 +69,12 @@ export async function signup(
|
||||
};
|
||||
}
|
||||
|
||||
// Get onboarding status from backend (includes chat flag evaluated for this user)
|
||||
const { shouldShowOnboarding } = await getOnboardingStatus();
|
||||
const next = shouldShowOnboarding ? "/onboarding" : "/";
|
||||
|
||||
return { success: true, next };
|
||||
return {
|
||||
success: true,
|
||||
next: shouldShowOnboarding ? "/onboarding" : "/copilot",
|
||||
};
|
||||
} catch (err) {
|
||||
Sentry.captureException(err);
|
||||
return {
|
||||
|
||||
@@ -106,8 +106,6 @@ export function useSignupPage() {
|
||||
data.agreeToTerms,
|
||||
);
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
if (!result.success) {
|
||||
if (result.error === "user_already_exists") {
|
||||
setFeedback("User with this email already exists");
|
||||
@@ -141,6 +139,10 @@ export function useSignupPage() {
|
||||
: "Unexpected error during signup",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { InfiniteData } from "@tanstack/react-query";
|
||||
import {
|
||||
getV1IsOnboardingEnabled,
|
||||
getV1OnboardingState,
|
||||
} from "./__generated__/endpoints/onboarding/onboarding";
|
||||
import { getV1CheckIfOnboardingIsCompleted } from "./__generated__/endpoints/onboarding/onboarding";
|
||||
import { Pagination } from "./__generated__/models/pagination";
|
||||
|
||||
export type OKData<TResponse extends { status: number; data?: any }> =
|
||||
@@ -178,10 +175,8 @@ export async function resolveResponse<
|
||||
}
|
||||
|
||||
export async function getOnboardingStatus() {
|
||||
const status = await resolveResponse(getV1IsOnboardingEnabled());
|
||||
const onboarding = await resolveResponse(getV1OnboardingState());
|
||||
const isCompleted = onboarding.completedSteps.includes("CONGRATS");
|
||||
const status = await resolveResponse(getV1CheckIfOnboardingIsCompleted());
|
||||
return {
|
||||
shouldShowOnboarding: status.is_onboarding_enabled && !isCompleted,
|
||||
shouldShowOnboarding: !status.is_completed,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5348,11 +5348,11 @@
|
||||
"security": [{ "HTTPBearerJWT": [] }]
|
||||
}
|
||||
},
|
||||
"/api/onboarding/enabled": {
|
||||
"/api/onboarding/completed": {
|
||||
"get": {
|
||||
"tags": ["v1", "onboarding", "public"],
|
||||
"summary": "Is onboarding enabled",
|
||||
"operationId": "getV1Is onboarding enabled",
|
||||
"summary": "Check if onboarding is completed",
|
||||
"operationId": "getV1Check if onboarding is completed",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
@@ -5371,6 +5371,41 @@
|
||||
"security": [{ "HTTPBearerJWT": [] }]
|
||||
}
|
||||
},
|
||||
"/api/onboarding/profile": {
|
||||
"post": {
|
||||
"tags": ["v1", "onboarding"],
|
||||
"summary": "Submit onboarding profile",
|
||||
"operationId": "postV1Submit onboarding profile",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OnboardingProfileRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": { "application/json": { "schema": {} } }
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/HTTP401NotAuthenticatedError"
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [{ "HTTPBearerJWT": [] }]
|
||||
}
|
||||
},
|
||||
"/api/onboarding/reset": {
|
||||
"post": {
|
||||
"tags": ["v1", "onboarding"],
|
||||
@@ -11182,18 +11217,40 @@
|
||||
"title": "OAuthApplicationPublicInfo",
|
||||
"description": "Public information about an OAuth application (for consent screen)"
|
||||
},
|
||||
"OnboardingStatusResponse": {
|
||||
"OnboardingProfileRequest": {
|
||||
"properties": {
|
||||
"is_onboarding_enabled": {
|
||||
"type": "boolean",
|
||||
"title": "Is Onboarding Enabled"
|
||||
"user_name": {
|
||||
"type": "string",
|
||||
"maxLength": 100,
|
||||
"minLength": 1,
|
||||
"title": "User Name"
|
||||
},
|
||||
"is_chat_enabled": { "type": "boolean", "title": "Is Chat Enabled" }
|
||||
"user_role": {
|
||||
"type": "string",
|
||||
"maxLength": 100,
|
||||
"minLength": 1,
|
||||
"title": "User Role"
|
||||
},
|
||||
"pain_points": {
|
||||
"items": { "type": "string" },
|
||||
"type": "array",
|
||||
"maxItems": 20,
|
||||
"title": "Pain Points"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["is_onboarding_enabled", "is_chat_enabled"],
|
||||
"required": ["user_name", "user_role"],
|
||||
"title": "OnboardingProfileRequest",
|
||||
"description": "Request body for onboarding profile submission."
|
||||
},
|
||||
"OnboardingStatusResponse": {
|
||||
"properties": {
|
||||
"is_completed": { "type": "boolean", "title": "Is Completed" }
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["is_completed"],
|
||||
"title": "OnboardingStatusResponse",
|
||||
"description": "Response for onboarding status check."
|
||||
"description": "Response for onboarding completion check."
|
||||
},
|
||||
"OnboardingStep": {
|
||||
"type": "string",
|
||||
|
||||
@@ -46,14 +46,6 @@ export default async function RootLayout({
|
||||
className={`${fonts.poppins.variable} ${fonts.sans.variable} ${fonts.mono.variable}`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<head>
|
||||
<SetupAnalytics
|
||||
host={host}
|
||||
ga={{
|
||||
gaId: process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || "G-FH2XK2W4GN",
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body className="min-h-screen">
|
||||
<ErrorBoundary context="application">
|
||||
<Providers
|
||||
@@ -63,6 +55,13 @@ export default async function RootLayout({
|
||||
// enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<SetupAnalytics
|
||||
host={host}
|
||||
ga={{
|
||||
gaId:
|
||||
process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || "G-FH2XK2W4GN",
|
||||
}}
|
||||
/>
|
||||
<div className="flex min-h-screen flex-col items-stretch justify-items-stretch">
|
||||
{children}
|
||||
<TallyPopupSimple />
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
interface Props extends React.SVGProps<SVGSVGElement> {
|
||||
hideText?: boolean;
|
||||
}
|
||||
|
||||
export function AutoGPTLogo({ hideText = false, className, ...props }: Props) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 89 40"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="AutoGPT Logo"
|
||||
className={className ?? "h-10 w-[5.5rem]"}
|
||||
{...props}
|
||||
>
|
||||
<g id="AutoGPT-logo 1" clipPath="url(#clip0_3364_2463)">
|
||||
<path
|
||||
id="Vector"
|
||||
d="M69.1364 28.8681V38.6414C69.1364 39.3617 68.5471 39.951 67.8301 39.951C67.0541 39.951 66.4124 39.4599 66.4124 38.6414V24.0584C66.4124 20.9644 68.9236 18.4531 72.0177 18.4531C75.1117 18.4531 77.623 20.9644 77.623 24.0584C77.623 27.1525 75.1117 29.6637 72.0177 29.6637C70.9634 29.6637 69.9812 29.3723 69.1397 28.8681H69.1364ZM70.2856 22.3231C71.2417 22.3231 72.0177 23.0991 72.0177 24.0552C72.0177 25.0112 71.2417 25.7872 70.2856 25.7872C70.1088 25.7872 69.9353 25.761 69.7749 25.7119C70.2824 26.3994 71.0976 26.8447 72.0177 26.8447C73.5565 26.8447 74.8039 25.5973 74.8039 24.0584C74.8039 22.5196 73.5565 21.2721 72.0177 21.2721C71.0976 21.2721 70.2824 21.7174 69.7749 22.405C69.9353 22.3559 70.1088 22.3297 70.2856 22.3297V22.3231Z"
|
||||
fill="url(#paint0_linear_3364_2463)"
|
||||
/>
|
||||
<path
|
||||
id="Vector_2"
|
||||
d="M62.133 28.8675V35.144C62.133 35.7137 61.9005 36.2343 61.524 36.6075C60.6989 37.4326 59.1699 37.4326 58.3448 36.6075C57.2611 35.5238 58.2891 33.6903 56.3509 31.752C54.4126 29.8137 51.1974 29.8694 49.318 31.752C48.4504 32.6196 47.9102 33.8212 47.9102 35.144C47.9102 35.8643 48.4995 36.4536 49.2198 36.4536C49.999 36.4536 50.6375 35.9625 50.6375 35.144C50.6375 34.5743 50.87 34.057 51.2465 33.6805C52.0716 32.8554 53.6006 32.8554 54.4257 33.6805C55.6076 34.8624 54.4126 36.5289 56.4196 38.536C58.3022 40.4186 61.5731 40.4186 63.4524 38.536C64.3201 37.6683 64.8603 36.4667 64.8603 35.144V24.0545C64.8603 20.9605 62.3491 18.4492 59.255 18.4492C56.161 18.4492 53.6497 20.9605 53.6497 24.0545C53.6497 27.1486 56.161 29.6598 59.255 29.6598C60.3093 29.6598 61.2948 29.3684 62.133 28.8642V28.8675ZM59.255 26.8441C58.335 26.8441 57.5197 26.3988 57.0122 25.7112C57.1727 25.7603 57.3462 25.7865 57.523 25.7865C58.479 25.7865 59.255 25.0106 59.255 24.0545C59.255 23.0985 58.479 22.3225 57.523 22.3225C57.3462 22.3225 57.1727 22.3487 57.0122 22.3978C57.5197 21.7103 58.335 21.265 59.255 21.265C60.7938 21.265 62.0413 22.5124 62.0413 24.0512C62.0413 25.5901 60.7938 26.8375 59.255 26.8375V26.8441Z"
|
||||
fill="url(#paint1_linear_3364_2463)"
|
||||
/>
|
||||
<path
|
||||
id="Vector_3"
|
||||
d="M81.709 12.959C81.709 9.51134 80.3371 6.24048 77.9045 3.80453C75.4685 1.36858 72.1977 0 68.75 0C65.3024 0 62.0315 1.37186 59.5956 3.80453C57.1596 6.24048 55.791 9.51461 55.791 12.959V13.5451C55.791 14.2948 56.4 14.9038 57.1498 14.9038C57.8996 14.9038 58.5085 14.2948 58.5085 13.5451V12.959C58.5085 10.2349 59.5956 7.64836 61.5175 5.72645C63.4394 3.80453 66.0259 2.71425 68.75 2.71425C71.4741 2.71425 74.0574 3.80126 75.9826 5.72645C77.9045 7.64836 78.9948 10.2349 78.9948 12.959C78.9948 13.7088 79.6037 14.3178 80.3535 14.3178C81.1033 14.3178 81.7123 13.7088 81.7123 12.959H81.709Z"
|
||||
fill="url(#paint2_linear_3364_2463)"
|
||||
/>
|
||||
<path
|
||||
id="Vector_4"
|
||||
d="M81.7092 17.061V18.7341H83.8963C84.6232 18.7341 85.2191 19.33 85.2191 20.0569C85.2191 20.7837 84.6952 21.4582 83.8963 21.4582H81.7092V35.1964C81.7092 35.7661 81.9417 36.2834 82.3182 36.6599C83.1433 37.485 84.6723 37.485 85.4974 36.6599C85.8739 36.2834 86.1064 35.7661 86.1064 35.1964V34.738C86.1064 33.9228 86.7481 33.4284 87.5241 33.4284C88.2444 33.4284 88.8337 34.0177 88.8337 34.738V35.1964C88.8337 36.5192 88.2935 37.7208 87.4258 38.5884C85.5432 40.471 82.2822 40.471 80.3996 38.5884C79.5319 37.7208 78.9917 36.5192 78.9917 35.1964V17.061C78.9917 16.272 79.6171 15.7383 80.3832 15.7383C81.1493 15.7383 81.706 16.3342 81.706 17.061H81.7092Z"
|
||||
fill="url(#paint3_linear_3364_2463)"
|
||||
/>
|
||||
<path
|
||||
id="Vector_5"
|
||||
d="M75.4293 38.6377C75.4293 39.358 74.8399 39.9441 74.1196 39.9441C73.3436 39.9441 72.7019 39.453 72.7019 38.6377V34.2013C72.7019 33.4809 73.2912 32.8916 74.0116 32.8916C74.7875 32.8916 75.4293 33.3827 75.4293 34.2013V38.6377Z"
|
||||
fill="url(#paint4_linear_3364_2463)"
|
||||
/>
|
||||
{!hideText && (
|
||||
<path
|
||||
id="Vector_6"
|
||||
d="M11.7672 22.2907V31.6252H8.94164V26.9399H2.82557V31.6252H0V22.2907C0 14.5998 11.7672 14.4983 11.7672 22.2907ZM44.3808 31.6252C48.5618 31.6252 51.9506 28.2365 51.9506 24.0554C51.9506 19.8744 48.5618 16.4857 44.3808 16.4857C40.1997 16.4857 36.811 19.8744 36.811 24.0554C36.811 28.2365 40.1997 31.6252 44.3808 31.6252ZM44.3808 28.7309C41.8008 28.7309 39.7086 26.6387 39.7086 24.0587C39.7086 21.4787 41.8008 19.3865 44.3808 19.3865C46.9608 19.3865 49.053 21.4787 49.053 24.0587C49.053 26.6387 46.9608 28.7309 44.3808 28.7309ZM37.3218 16.4857V19.2097H33.2095V31.6252H30.4854V19.2097H26.3731V16.4857H37.3218ZM25.0111 25.8202V16.4857H22.1855V25.8202C22.1855 30.0242 16.0661 29.9489 16.0661 25.8202V16.4857H13.2406V25.8202C13.2406 33.5111 25.0078 33.6126 25.0078 25.8202H25.0111ZM8.94164 24.2159V22.294C8.94164 18.09 2.8223 18.1653 2.8223 22.294V24.2159H8.94164Z"
|
||||
fill="#000030"
|
||||
/>
|
||||
)}
|
||||
<path
|
||||
id="Vector_7"
|
||||
d="M87.4713 32.257C88.2434 32.257 88.8693 31.6311 88.8693 30.859C88.8693 30.0869 88.2434 29.4609 87.4713 29.4609C86.6992 29.4609 86.0732 30.0869 86.0732 30.859C86.0732 31.6311 86.6992 32.257 87.4713 32.257Z"
|
||||
fill="#669CF6"
|
||||
/>
|
||||
<path
|
||||
id="Vector_8"
|
||||
d="M49.2167 39.9475C49.9888 39.9475 50.6147 39.3215 50.6147 38.5494C50.6147 37.7773 49.9888 37.1514 49.2167 37.1514C48.4445 37.1514 47.8186 37.7773 47.8186 38.5494C47.8186 39.3215 48.4445 39.9475 49.2167 39.9475Z"
|
||||
fill="#669CF6"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_3364_2463"
|
||||
x1="62.7328"
|
||||
y1="20.9589"
|
||||
x2="62.7328"
|
||||
y2="33.2932"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#000030" />
|
||||
<stop offset="1" stopColor="#9900FF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_3364_2463"
|
||||
x1="47.5336"
|
||||
y1="20.947"
|
||||
x2="47.5336"
|
||||
y2="33.2951"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#000030" />
|
||||
<stop offset="1" stopColor="#4285F4" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_3364_2463"
|
||||
x1="69.4138"
|
||||
y1="6.17402"
|
||||
x2="48.0898"
|
||||
y2="-3.94009"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#4285F4" />
|
||||
<stop offset="1" stopColor="#9900FF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint3_linear_3364_2463"
|
||||
x1="74.2976"
|
||||
y1="15.7136"
|
||||
x2="74.2976"
|
||||
y2="34.5465"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#000030" />
|
||||
<stop offset="1" stopColor="#4285F4" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint4_linear_3364_2463"
|
||||
x1="64.3579"
|
||||
y1="24.1914"
|
||||
x2="65.0886"
|
||||
y2="30.9756"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#4285F4" />
|
||||
<stop offset="1" stopColor="#9900FF" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_3364_2463">
|
||||
<rect width="88.8696" height="40" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
import { Emoji } from "./Emoji";
|
||||
|
||||
const meta: Meta<typeof Emoji> = {
|
||||
title: "Atoms/Emoji",
|
||||
tags: ["autodocs"],
|
||||
component: Emoji,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Renders emoji text as cross-platform SVG images using Twemoji.",
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
text: {
|
||||
control: "text",
|
||||
description: "Emoji character(s) to render",
|
||||
},
|
||||
size: {
|
||||
control: { type: "number", min: 12, max: 96, step: 4 },
|
||||
description: "Size in pixels (width and height)",
|
||||
},
|
||||
},
|
||||
args: {
|
||||
text: "🚀",
|
||||
size: 24,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
text: "🚀",
|
||||
},
|
||||
};
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
text: "✨",
|
||||
size: 16,
|
||||
},
|
||||
};
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
text: "🎉",
|
||||
size: 48,
|
||||
},
|
||||
};
|
||||
|
||||
export const AllSizes: Story = {
|
||||
render: renderAllSizes,
|
||||
};
|
||||
|
||||
function renderAllSizes() {
|
||||
const sizes = [16, 24, 32, 48, 64];
|
||||
return (
|
||||
<div className="flex items-end gap-4">
|
||||
{sizes.map((size) => (
|
||||
<div key={size} className="flex flex-col items-center gap-2">
|
||||
<Emoji text="🔥" size={size} />
|
||||
<span className="text-xs text-muted-foreground">{size}px</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const MultipleEmojis: Story = {
|
||||
render: renderMultipleEmojis,
|
||||
};
|
||||
|
||||
function renderMultipleEmojis() {
|
||||
const emojis = ["😀", "🎯", "⚡", "🌈", "🤖", "💡", "🔧", "📦"];
|
||||
return (
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{emojis.map((emoji) => (
|
||||
<Emoji key={emoji} text={emoji} size={32} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import twemoji from "twemoji";
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export function Emoji({ text, size = 24 }: Props) {
|
||||
return (
|
||||
<span
|
||||
className="inline-flex items-center"
|
||||
style={{ width: size, height: size }}
|
||||
dangerouslySetInnerHTML={{
|
||||
// twemoji.parse only converts emoji codepoints to <img> tags
|
||||
// pointing to Twitter's CDN SVGs — it does not inject arbitrary HTML
|
||||
__html: twemoji.parse(text, {
|
||||
folder: "svg",
|
||||
ext: ".svg",
|
||||
attributes: () => ({
|
||||
width: String(size),
|
||||
height: String(size),
|
||||
}),
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { FadeIn } from "./FadeIn";
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
|
||||
const meta: Meta<typeof FadeIn> = {
|
||||
title: "Atoms/FadeIn",
|
||||
tags: ["autodocs"],
|
||||
component: FadeIn,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"A wrapper that fades in its children with a subtle upward slide animation using framer-motion.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<FadeIn>
|
||||
<div className="rounded-lg border border-zinc-200 bg-white p-8 text-center">
|
||||
<p className="text-lg font-medium">This content fades in</p>
|
||||
<p className="text-sm text-zinc-500">
|
||||
With a subtle upward slide animation
|
||||
</p>
|
||||
</div>
|
||||
</FadeIn>
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
onComplete?: () => void;
|
||||
}
|
||||
|
||||
export function FadeIn({ children, onComplete }: Props) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
||||
onAnimationComplete={onComplete}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -225,7 +225,7 @@ export function Input({
|
||||
return hideLabel ? (
|
||||
inputWithError
|
||||
) : (
|
||||
<label htmlFor={props.id} className="flex flex-col gap-2">
|
||||
<label htmlFor={props.id} className="flex w-full flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Text variant="large-medium" as="span" className="text-black">
|
||||
{label}
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
import { useGetV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/store";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import { IconAutoGPTLogo, IconType } from "@/components/__legacy__/ui/icons";
|
||||
import { IconType } from "@/components/__legacy__/ui/icons";
|
||||
import { AutoGPTLogo } from "@/components/atoms/AutoGPTLogo/AutoGPTLogo";
|
||||
import { PreviewBanner } from "@/components/layout/Navbar/components/PreviewBanner/PreviewBanner";
|
||||
import { isLogoutInProgress } from "@/lib/autogpt-server-api/helpers";
|
||||
import { NAVBAR_HEIGHT_PX } from "@/lib/constants";
|
||||
import { useBreakpoint } from "@/lib/hooks/useBreakpoint";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { environment } from "@/services/environment";
|
||||
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
|
||||
import { AccountMenu } from "./components/AccountMenu/AccountMenu";
|
||||
import { FeedbackButton } from "./components/FeedbackButton";
|
||||
import { AgentActivityDropdown } from "./components/AgentActivityDropdown/AgentActivityDropdown";
|
||||
@@ -25,7 +25,6 @@ export function Navbar() {
|
||||
const breakpoint = useBreakpoint();
|
||||
const isSmallScreen = breakpoint === "sm" || breakpoint === "base";
|
||||
const dynamicMenuItems = getAccountMenuItems(user?.role);
|
||||
const isChatEnabled = useGetFlag(Flag.CHAT);
|
||||
const previewBranchName = environment.getPreviewStealingDev();
|
||||
const logoutInProgress = isLogoutInProgress();
|
||||
|
||||
@@ -44,11 +43,9 @@ export function Navbar() {
|
||||
|
||||
const shouldShowPreviewBanner = Boolean(isLoggedIn && previewBranchName);
|
||||
|
||||
const homeHref = isChatEnabled === true ? "/copilot" : "/library";
|
||||
|
||||
const actualLoggedInLinks = [
|
||||
{ name: "Home", href: homeHref },
|
||||
...(isChatEnabled === true ? [{ name: "Agents", href: "/library" }] : []),
|
||||
{ name: "Home", href: "/copilot" },
|
||||
{ name: "Agents", href: "/library" },
|
||||
...loggedInLinks,
|
||||
];
|
||||
|
||||
@@ -88,8 +85,8 @@ export function Navbar() {
|
||||
) : null}
|
||||
|
||||
{/* Centered logo */}
|
||||
<div className="static h-auto w-[4.5rem] md:absolute md:left-1/2 md:top-1/2 md:w-[5.5rem] md:-translate-x-1/2 md:-translate-y-1/2">
|
||||
<IconAutoGPTLogo className="h-full w-full" />
|
||||
<div className="static md:absolute md:left-1/2 md:top-1/2 md:-translate-x-1/2 md:-translate-y-1/2">
|
||||
<AutoGPTLogo className="h-auto w-[4.5rem] md:w-[5.5rem]" />
|
||||
</div>
|
||||
|
||||
{/* Right section */}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
|
||||
import { Laptop, ListChecksIcon } from "@phosphor-icons/react/dist/ssr";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
@@ -22,12 +21,10 @@ interface Props {
|
||||
|
||||
export function NavbarLink({ name, href }: Props) {
|
||||
const pathname = usePathname();
|
||||
const isChatEnabled = useGetFlag(Flag.CHAT);
|
||||
const expectedHomeRoute = isChatEnabled ? "/copilot" : "/library";
|
||||
|
||||
const isActive =
|
||||
href === expectedHomeRoute
|
||||
? pathname === "/" || pathname.startsWith(expectedHomeRoute)
|
||||
href === "/copilot"
|
||||
? pathname === "/" || pathname.startsWith("/copilot")
|
||||
: pathname.includes(href);
|
||||
|
||||
return (
|
||||
@@ -77,24 +74,14 @@ export function NavbarLink({ name, href }: Props) {
|
||||
<HomepageIcon />
|
||||
</div>
|
||||
)}
|
||||
{href === "/library" &&
|
||||
(isChatEnabled ? (
|
||||
<ListChecksIcon
|
||||
className={cn(
|
||||
"h-5 w-5 shrink-0",
|
||||
isActive && "text-white dark:text-black",
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
iconNudgedClass,
|
||||
isActive && "text-white dark:text-black",
|
||||
)}
|
||||
>
|
||||
<HomepageIcon />
|
||||
</div>
|
||||
))}
|
||||
{href === "/library" && (
|
||||
<ListChecksIcon
|
||||
className={cn(
|
||||
"h-5 w-5 shrink-0",
|
||||
isActive && "text-white dark:text-black",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
variant="h5"
|
||||
className={cn(
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { TypingText } from "./TypingText";
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
|
||||
const meta: Meta<typeof TypingText> = {
|
||||
title: "Molecules/TypingText",
|
||||
tags: ["autodocs"],
|
||||
component: TypingText,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Animates text appearing character by character with a blinking cursor. Useful for loading states and onboarding flows.",
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
text: { control: "text" },
|
||||
active: { control: "boolean" },
|
||||
speed: { control: { type: "range", min: 10, max: 100, step: 5 } },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
text: "Personalizing your experience...",
|
||||
active: true,
|
||||
speed: 30,
|
||||
},
|
||||
};
|
||||
|
||||
export const Fast: Story = {
|
||||
args: {
|
||||
text: "This types out quickly!",
|
||||
active: true,
|
||||
speed: 15,
|
||||
},
|
||||
};
|
||||
|
||||
export const Slow: Story = {
|
||||
args: {
|
||||
text: "This types out slowly...",
|
||||
active: true,
|
||||
speed: 80,
|
||||
},
|
||||
};
|
||||
|
||||
export const Inactive: Story = {
|
||||
args: {
|
||||
text: "This text is hidden because active is false",
|
||||
active: false,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
active: boolean;
|
||||
speed?: number;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export function TypingText({ text, active, speed = 30, delay = 0 }: Props) {
|
||||
const [charCount, setCharCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) {
|
||||
setCharCount(0);
|
||||
return;
|
||||
}
|
||||
|
||||
setCharCount(0);
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
const interval = setInterval(() => {
|
||||
setCharCount((prev) => {
|
||||
if (prev >= text.length) {
|
||||
clearInterval(interval);
|
||||
return prev;
|
||||
}
|
||||
return prev + 1;
|
||||
});
|
||||
}, speed);
|
||||
|
||||
cleanupRef = () => clearInterval(interval);
|
||||
}, delay);
|
||||
|
||||
let cleanupRef = () => {};
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
cleanupRef();
|
||||
};
|
||||
}, [active, text, speed, delay]);
|
||||
|
||||
if (!active) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{text.slice(0, charCount)}
|
||||
{charCount < text.length && (
|
||||
<span className="inline-block h-4 w-[2px] animate-pulse bg-current" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -36,7 +36,7 @@ export function shouldRedirectFromOnboarding(
|
||||
pathname: string,
|
||||
): boolean {
|
||||
return (
|
||||
completedSteps.includes("CONGRATS") &&
|
||||
completedSteps.includes("VISIT_COPILOT") &&
|
||||
!pathname.startsWith("/onboarding/reset")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
import {
|
||||
getV1IsOnboardingEnabled,
|
||||
getV1CheckIfOnboardingIsCompleted,
|
||||
getV1OnboardingState,
|
||||
patchV1UpdateOnboardingState,
|
||||
postV1CompleteOnboardingStep,
|
||||
@@ -142,13 +142,16 @@ export default function OnboardingProvider({
|
||||
|
||||
async function initializeOnboarding() {
|
||||
try {
|
||||
// Check onboarding enabled only for onboarding routes
|
||||
if (isOnOnboardingRoute) {
|
||||
const enabled = await resolveResponse(getV1IsOnboardingEnabled());
|
||||
if (!enabled) {
|
||||
router.push("/");
|
||||
return;
|
||||
}
|
||||
const { is_completed } = await resolveResponse(
|
||||
getV1CheckIfOnboardingIsCompleted(),
|
||||
);
|
||||
|
||||
if (!is_completed && !isOnOnboardingRoute) {
|
||||
router.replace("/onboarding");
|
||||
return;
|
||||
} else if (is_completed && isOnOnboardingRoute) {
|
||||
router.replace("/copilot");
|
||||
return;
|
||||
}
|
||||
|
||||
const onboarding = await fetchOnboarding();
|
||||
@@ -158,7 +161,7 @@ export default function OnboardingProvider({
|
||||
isOnOnboardingRoute &&
|
||||
shouldRedirectFromOnboarding(onboarding.completedSteps, pathname)
|
||||
) {
|
||||
router.push("/");
|
||||
router.replace("/copilot");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize onboarding:", error);
|
||||
|
||||
@@ -13,8 +13,7 @@ export enum Flag {
|
||||
AGENT_FAVORITING = "agent-favoriting",
|
||||
MARKETPLACE_SEARCH_TERMS = "marketplace-search-terms",
|
||||
ENABLE_PLATFORM_PAYMENT = "enable-platform-payment",
|
||||
CHAT = "chat",
|
||||
CHAT_MODE_OPTION = "copilot-fast-mode-option",
|
||||
CHAT_MODE_OPTION = "chat-mode-option",
|
||||
}
|
||||
|
||||
const isPwMockEnabled = process.env.NEXT_PUBLIC_PW_TEST === "true";
|
||||
@@ -28,7 +27,6 @@ const defaultFlags = {
|
||||
[Flag.AGENT_FAVORITING]: false,
|
||||
[Flag.MARKETPLACE_SEARCH_TERMS]: DEFAULT_SEARCH_TERMS,
|
||||
[Flag.ENABLE_PLATFORM_PAYMENT]: false,
|
||||
[Flag.CHAT]: false,
|
||||
[Flag.CHAT_MODE_OPTION]: false,
|
||||
};
|
||||
|
||||
|
||||
120
autogpt_platform/frontend/src/tests/onboarding.spec.ts
Normal file
120
autogpt_platform/frontend/src/tests/onboarding.spec.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import test, { expect } from "@playwright/test";
|
||||
import { signupTestUser } from "./utils/signup";
|
||||
import { completeOnboardingWizard } from "./utils/onboarding";
|
||||
import { getSelectors } from "./utils/selectors";
|
||||
|
||||
test("new user completes full onboarding wizard", async ({ page }) => {
|
||||
// Signup WITHOUT skipping onboarding (ignoreOnboarding=false)
|
||||
await signupTestUser(page, undefined, undefined, false);
|
||||
|
||||
// Should be on onboarding
|
||||
await expect(page).toHaveURL(/\/onboarding/);
|
||||
|
||||
// Complete the wizard
|
||||
await completeOnboardingWizard(page, {
|
||||
name: "Alice",
|
||||
role: "Marketing",
|
||||
painPoints: ["Social media", "Email & outreach"],
|
||||
});
|
||||
|
||||
// Should have been redirected to /copilot
|
||||
await expect(page).toHaveURL(/\/copilot/);
|
||||
|
||||
// User should be authenticated
|
||||
await page
|
||||
.getByTestId("profile-popout-menu-trigger")
|
||||
.waitFor({ state: "visible", timeout: 10000 });
|
||||
});
|
||||
|
||||
test("onboarding wizard step navigation works", async ({ page }) => {
|
||||
await signupTestUser(page, undefined, undefined, false);
|
||||
await expect(page).toHaveURL(/\/onboarding/);
|
||||
|
||||
// Step 1: Welcome
|
||||
await expect(page.getByText("Welcome to AutoGPT")).toBeVisible();
|
||||
await page.getByLabel("Your first name").fill("Bob");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Step 2: Role — verify we're here, then go back
|
||||
await expect(page.getByText("What best describes you")).toBeVisible();
|
||||
await page.getByText("Back").click();
|
||||
|
||||
// Should be back on step 1 with name preserved
|
||||
await expect(page.getByText("Welcome to AutoGPT")).toBeVisible();
|
||||
await expect(page.getByLabel("Your first name")).toHaveValue("Bob");
|
||||
});
|
||||
|
||||
test("onboarding wizard validates required fields", async ({ page }) => {
|
||||
await signupTestUser(page, undefined, undefined, false);
|
||||
await expect(page).toHaveURL(/\/onboarding/);
|
||||
|
||||
// Step 1: Continue should be disabled without a name
|
||||
const continueButton = page.getByRole("button", { name: "Continue" });
|
||||
await expect(continueButton).toBeDisabled();
|
||||
|
||||
// Fill name — continue should become enabled
|
||||
await page.getByLabel("Your first name").fill("Charlie");
|
||||
await expect(continueButton).toBeEnabled();
|
||||
await continueButton.click();
|
||||
|
||||
// Step 2: Continue should be disabled without a role
|
||||
const step2Continue = page.getByRole("button", { name: "Continue" });
|
||||
await expect(step2Continue).toBeDisabled();
|
||||
|
||||
// Select role — continue should become enabled
|
||||
await page.getByText("Engineering").click();
|
||||
await expect(step2Continue).toBeEnabled();
|
||||
await step2Continue.click();
|
||||
|
||||
// Step 3: Launch Autopilot should be disabled without any pain points
|
||||
const launchButton = page.getByRole("button", { name: "Launch Autopilot" });
|
||||
await expect(launchButton).toBeDisabled();
|
||||
|
||||
// Select a pain point — button should become enabled
|
||||
await page.getByText("Research", { exact: true }).click();
|
||||
await expect(launchButton).toBeEnabled();
|
||||
});
|
||||
|
||||
test("completed onboarding redirects away from /onboarding", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Create user and complete onboarding
|
||||
await signupTestUser(page, undefined, undefined, false);
|
||||
await completeOnboardingWizard(page);
|
||||
|
||||
// Try to navigate back to onboarding — should be redirected to /copilot
|
||||
await page.goto("http://localhost:3000/onboarding");
|
||||
await page.waitForURL(/\/copilot/, { timeout: 10000 });
|
||||
});
|
||||
|
||||
test("onboarding URL params sync with steps", async ({ page }) => {
|
||||
await signupTestUser(page, undefined, undefined, false);
|
||||
await expect(page).toHaveURL(/\/onboarding/);
|
||||
|
||||
// Step 1: URL may or may not include step=1 on initial load (no param is equivalent to step 1)
|
||||
await expect(page.getByText("Welcome to AutoGPT")).toBeVisible();
|
||||
|
||||
// Fill name and go to step 2
|
||||
await page.getByLabel("Your first name").fill("Test");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// URL should show step=2
|
||||
await expect(page).toHaveURL(/step=2/);
|
||||
});
|
||||
|
||||
test("role-based pain point ordering works", async ({ page }) => {
|
||||
await signupTestUser(page, undefined, undefined, false);
|
||||
|
||||
// Complete step 1
|
||||
await page.getByLabel("Your first name").fill("Test");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Select Sales/BD role
|
||||
await page.getByText("Sales / BD").click();
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// On pain points step, "Finding leads" should be visible (top pick for Sales)
|
||||
await expect(page.getByText("What's eating your time?")).toBeVisible();
|
||||
const { getText } = getSelectors(page);
|
||||
await expect(getText("Finding leads")).toBeVisible();
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Page } from "@playwright/test";
|
||||
import { skipOnboardingIfPresent } from "../utils/onboarding";
|
||||
|
||||
export class LoginPage {
|
||||
constructor(private page: Page) {}
|
||||
@@ -64,6 +65,9 @@ export class LoginPage {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200)); // allow time for client-side redirect
|
||||
await this.page.waitForLoadState("load", { timeout: 10_000 });
|
||||
|
||||
// If redirected to onboarding, complete it via API so tests can proceed
|
||||
await skipOnboardingIfPresent(this.page, "/marketplace");
|
||||
|
||||
console.log("➡️ Navigating to /marketplace ...");
|
||||
await this.page.goto("/marketplace", { timeout: 20_000 });
|
||||
console.log("✅ Login process complete");
|
||||
|
||||
@@ -182,10 +182,10 @@ test("logged in user is redirected from /login to /copilot", async ({
|
||||
await hasUrl(page, "/marketplace");
|
||||
|
||||
await page.goto("/login");
|
||||
await hasUrl(page, "/library?sort=updatedAt");
|
||||
await hasUrl(page, "/copilot");
|
||||
});
|
||||
|
||||
test("logged in user is redirected from /signup to /library", async ({
|
||||
test("logged in user is redirected from /signup to /copilot", async ({
|
||||
page,
|
||||
}) => {
|
||||
const testUser = await getTestUser();
|
||||
@@ -195,5 +195,5 @@ test("logged in user is redirected from /signup to /library", async ({
|
||||
await hasUrl(page, "/marketplace");
|
||||
|
||||
await page.goto("/signup");
|
||||
await hasUrl(page, "/library?sort=updatedAt");
|
||||
await hasUrl(page, "/copilot");
|
||||
});
|
||||
|
||||
81
autogpt_platform/frontend/src/tests/utils/onboarding.ts
Normal file
81
autogpt_platform/frontend/src/tests/utils/onboarding.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Page, expect } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Complete the onboarding wizard via API.
|
||||
* Use this when a test needs an authenticated user who has already finished onboarding
|
||||
* (e.g., tests that navigate to marketplace, library, or build pages).
|
||||
*
|
||||
* The function sends a POST request to the onboarding completion endpoint using
|
||||
* the page's request context, which inherits the browser's auth cookies.
|
||||
*/
|
||||
export async function completeOnboardingViaAPI(page: Page) {
|
||||
await page.request.post(
|
||||
"http://localhost:3000/api/proxy/api/onboarding/step?step=VISIT_COPILOT",
|
||||
{ headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the onboarding redirect that occurs after login/signup.
|
||||
* If the page is on /onboarding, completes onboarding via API and navigates
|
||||
* to the given destination. If already past onboarding, does nothing.
|
||||
*/
|
||||
export async function skipOnboardingIfPresent(
|
||||
page: Page,
|
||||
destination: string = "/marketplace",
|
||||
) {
|
||||
const url = page.url();
|
||||
if (!url.includes("/onboarding")) return;
|
||||
|
||||
await completeOnboardingViaAPI(page);
|
||||
await page.goto(`http://localhost:3000${destination}`);
|
||||
await page.waitForLoadState("domcontentloaded", { timeout: 10000 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk through the full 4-step onboarding wizard in the browser.
|
||||
* Returns the data that was entered so tests can verify it was submitted.
|
||||
*/
|
||||
export async function completeOnboardingWizard(
|
||||
page: Page,
|
||||
options?: {
|
||||
name?: string;
|
||||
role?: string;
|
||||
painPoints?: string[];
|
||||
},
|
||||
) {
|
||||
const name = options?.name ?? "TestUser";
|
||||
const role = options?.role ?? "Engineering";
|
||||
const painPoints = options?.painPoints ?? ["Research", "Reports & data"];
|
||||
|
||||
// Step 1: Welcome — enter name
|
||||
await expect(page.getByText("Welcome to AutoGPT")).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
await page.getByLabel("Your first name").fill(name);
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Step 2: Role — select a role
|
||||
await expect(page.getByText("What best describes you")).toBeVisible({
|
||||
timeout: 5000,
|
||||
});
|
||||
await page.getByText(role, { exact: false }).click();
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Step 3: Pain points — select tasks
|
||||
await expect(page.getByText("What's eating your time?")).toBeVisible({
|
||||
timeout: 5000,
|
||||
});
|
||||
for (const point of painPoints) {
|
||||
await page.getByText(point, { exact: true }).click();
|
||||
}
|
||||
await page.getByRole("button", { name: "Launch Autopilot" }).click();
|
||||
|
||||
// Step 4: Preparing — wait for animation to complete and redirect to /copilot
|
||||
await expect(page.getByText("Preparing your workspace")).toBeVisible({
|
||||
timeout: 5000,
|
||||
});
|
||||
await page.waitForURL(/\/copilot/, { timeout: 15000 });
|
||||
|
||||
return { name, role, painPoints };
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Page } from "@playwright/test";
|
||||
import { LoginPage } from "../pages/login.page";
|
||||
import { skipOnboardingIfPresent } from "./onboarding";
|
||||
|
||||
type TestUser = {
|
||||
email: string;
|
||||
@@ -25,8 +26,15 @@ export class SigninUtils {
|
||||
await this.page.goto("/login");
|
||||
await this.loginPage.login(testUser.email, testUser.password);
|
||||
|
||||
// Verify we're on marketplace
|
||||
await this.page.waitForURL("/marketplace");
|
||||
// Wait for redirect — could land on /onboarding, /marketplace, or /copilot
|
||||
await this.page.waitForURL(
|
||||
(url: URL) =>
|
||||
/\/(onboarding|marketplace|copilot|library)/.test(url.pathname),
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
|
||||
// Skip onboarding if present
|
||||
await skipOnboardingIfPresent(this.page, "/marketplace");
|
||||
|
||||
// Verify profile menu is visible (user is authenticated)
|
||||
await this.page.getByTestId("profile-popout-menu-trigger").waitFor({
|
||||
|
||||
@@ -2,6 +2,7 @@ import { TestUser } from "./auth";
|
||||
import { getSelectors } from "./selectors";
|
||||
import { isVisible } from "./assertion";
|
||||
import { BuildPage } from "../pages/build.page";
|
||||
import { skipOnboardingIfPresent } from "./onboarding";
|
||||
|
||||
export async function signupTestUser(
|
||||
page: any,
|
||||
@@ -58,18 +59,15 @@ export async function signupTestUser(
|
||||
|
||||
// Handle onboarding redirect if needed
|
||||
if (currentUrl.includes("/onboarding") && ignoreOnboarding) {
|
||||
await page.goto("http://localhost:3000/marketplace");
|
||||
await page.waitForLoadState("domcontentloaded", { timeout: 10000 });
|
||||
await skipOnboardingIfPresent(page, "/marketplace");
|
||||
}
|
||||
|
||||
// Verify we're on an expected final page and user is authenticated
|
||||
if (currentUrl.includes("/copilot") || currentUrl.includes("/library")) {
|
||||
// For copilot/library landing pages, just verify user is authenticated
|
||||
await page
|
||||
.getByTestId("profile-popout-menu-trigger")
|
||||
.waitFor({ state: "visible", timeout: 10000 });
|
||||
} else if (ignoreOnboarding || currentUrl.includes("/marketplace")) {
|
||||
// Verify we're on marketplace
|
||||
await page
|
||||
.getByText(
|
||||
"Bringing you AI agents designed by thinkers from around the world",
|
||||
@@ -77,7 +75,6 @@ export async function signupTestUser(
|
||||
.first()
|
||||
.waitFor({ state: "visible", timeout: 10000 });
|
||||
|
||||
// Verify user is authenticated (profile menu visible)
|
||||
await page
|
||||
.getByTestId("profile-popout-menu-trigger")
|
||||
.waitFor({ state: "visible", timeout: 10000 });
|
||||
|
||||
Reference in New Issue
Block a user