diff --git a/ui/src/components/agents/AgentsHome.tsx b/ui/src/components/agents/AgentsHome.tsx index c1a0b5b2..1fd4174a 100644 --- a/ui/src/components/agents/AgentsHome.tsx +++ b/ui/src/components/agents/AgentsHome.tsx @@ -7,7 +7,7 @@ import type { AgentThread, ImageChunk } from "@/lib/agents/types" import type { CreateAgentThreadVariables } from "@/lib/agents/queries" import type { ModelSelection } from "@/lib/agents/provider/useModelOptions" import { AgentPromptBar } from "@/components/agents/AgentPromptBar" -import { SlackConnectDialog } from "@/components/agents/SlackConnectDialog" +import { OnboardingDialog } from "@/components/agents/OnboardingDialog" import { Logo } from "@/components/agents/ported/Logo" import { agentThreadKeys, @@ -107,7 +107,7 @@ export function AgentsHome() { return (
- +
diff --git a/ui/src/components/agents/OnboardingDialog.tsx b/ui/src/components/agents/OnboardingDialog.tsx new file mode 100644 index 00000000..457d7361 --- /dev/null +++ b/ui/src/components/agents/OnboardingDialog.tsx @@ -0,0 +1,205 @@ +import { Dialog } from "@base-ui/react/dialog" +import { useQuery } from "@tanstack/react-query" +import { useEffect, useState } from "react" +import { IoLogoSlack } from "react-icons/io5" + +import type { ModelOption } from "@/lib/api" +import { Button } from "@/components/ui/button" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { api, slackConnectUrl } from "@/lib/api" +import { + buildProfileUpdate, + useOptions, + useProfile, + useSaveProfile, +} from "@/lib/profile" +import { useSession } from "@/lib/session" + +/** + * First-run onboarding modal: pick a default agent model, then connect Slack. + * + * The model step shows until the user has saved a default model; the Slack step + * shows (where Sign in with Slack is enabled) until their Slack account is + * linked. Both steps live in the same dialog so a new user is walked through + * picking a model and connecting Slack in one place. Dismissing hides it for + * the session; an incomplete step reappears on the next login. + */ +export function OnboardingDialog() { + const session = useSession() + const mapping = useQuery({ queryKey: ["myMapping"], queryFn: api.myMapping }) + const profile = useProfile() + const options = useOptions() + const save = useSaveProfile() + const [dismissed, setDismissed] = useState(false) + const [error, setError] = useState(null) + + const firstModel: ModelOption | undefined = options.data?.models[0] + const defaultModel = options.data?.default_agent_model ?? firstModel?.id ?? "" + const defaultEffort = + options.data?.default_agent_reasoning_effort ?? + firstModel?.default_effort ?? + "" + const [modelId, setModelId] = useState("") + const [effort, setEffort] = useState("") + + useEffect(() => { + if (!modelId && defaultModel) setModelId(defaultModel) + }, [modelId, defaultModel]) + + const currentModel: ModelOption | undefined = + options.data?.models.find((m) => m.id === modelId) ?? firstModel + + useEffect(() => { + if (!currentModel) return + if (!effort || !currentModel.efforts.includes(effort)) { + setEffort(currentModel.default_effort) + } + }, [currentModel, effort]) + + const slackEnabled = session.data?.slack_oauth_enabled ?? false + const slackConnected = !!mapping.data?.slack_user_id + const hasDefaultModel = !!profile.data?.default_model + + const needsModel = + !profile.isLoading && profile.data !== undefined && !hasDefaultModel + const needsSlack = + slackEnabled && !slackConnected && !mapping.isLoading && !mapping.isError + const open = !dismissed && (needsModel || needsSlack) + const step: "model" | "slack" = needsModel ? "model" : "slack" + + const handleSaveModel = () => { + if (!modelId) return + setError(null) + save + .mutateAsync( + buildProfileUpdate( + profile.data, + { default_model: modelId, reasoning_effort: effort }, + defaultModel, + defaultEffort + ) + ) + .catch((e: Error) => setError(e.message)) + } + + return ( + { + if (!next) setDismissed(true) + }} + > + + + + {step === "model" ? ( +
+ + Choose your default model + + + Pick the model Open SWE uses when you don't specify one. You can + change this anytime in your settings. + +
+
+ Default model + +
+
+ Reasoning effort + +
+
+ {error &&

{error}

} +
+ + +
+
+ ) : ( +
+
+ + + Connect your Slack account + +
+ + Connect Slack so that when you tag Open SWE, it can resolve your + GitHub account. We use the email Slack verifies, which also lets + Linear mentions resolve to you. + +
+ + +
+
+ )} +
+
+
+ ) +} diff --git a/ui/src/components/agents/SlackConnectDialog.tsx b/ui/src/components/agents/SlackConnectDialog.tsx deleted file mode 100644 index 6ddcbb86..00000000 --- a/ui/src/components/agents/SlackConnectDialog.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { Dialog } from "@base-ui/react/dialog" -import { useQuery } from "@tanstack/react-query" -import { useState } from "react" -import { IoLogoSlack } from "react-icons/io5" - -import { Button } from "@/components/ui/button" -import { api, slackConnectUrl } from "@/lib/api" -import { useSession } from "@/lib/session" - -/** - * Modal shown on first login (and until connected) prompting the user to link - * Slack so Open SWE can resolve their GitHub account when tagged in Slack. - * ``open`` is derived from the mapping query, so it appears once the data - * resolves to "not connected" and closes itself once Slack is linked; dismissing - * it hides it for the session. - */ -export function SlackConnectDialog() { - const session = useSession() - const mapping = useQuery({ queryKey: ["myMapping"], queryFn: api.myMapping }) - const [dismissed, setDismissed] = useState(false) - - const slackEnabled = session.data?.slack_oauth_enabled ?? false - const connected = !!mapping.data?.slack_user_id - const shouldShow = - slackEnabled && !connected && !mapping.isLoading && !mapping.isError - const open = shouldShow && !dismissed - - return ( - { - if (!next) setDismissed(true) - }} - > - - - -
-
- - - Connect your Slack account - -
- - Connect Slack so that when you tag Open SWE, it can resolve your - GitHub account. We use the email Slack verifies, which also lets - Linear mentions resolve to you. - -
- - -
-
-
-
-
- ) -}