mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-01 06:13:15 +00:00
Lighter, more consistent type across the dashboard and agent UI: unify title sizes, drop semibold/bold in chrome to medium, harmonize stray text-sm to text-xs, normalize chat markdown headings, and enable antialiased font smoothing (fixes heavy text on dark backgrounds). Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
205 lines
7.7 KiB
TypeScript
205 lines
7.7 KiB
TypeScript
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<string | null>(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 (
|
|
<Dialog.Root
|
|
open={open}
|
|
onOpenChange={(next) => {
|
|
if (!next) setDismissed(true)
|
|
}}
|
|
>
|
|
<Dialog.Portal>
|
|
<Dialog.Backdrop className="fixed inset-0 z-50 bg-black/50 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0" />
|
|
<Dialog.Popup className="fixed top-1/2 left-1/2 z-50 w-[min(28rem,calc(100vw-2rem))] -translate-x-1/2 -translate-y-1/2 rounded-lg bg-popover p-6 text-popover-foreground shadow-md ring-1 ring-foreground/10 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95">
|
|
{step === "model" ? (
|
|
<div className="flex flex-col gap-4">
|
|
<Dialog.Title className="text-sm font-medium">
|
|
Choose your default model
|
|
</Dialog.Title>
|
|
<Dialog.Description className="text-xs text-muted-foreground">
|
|
Pick the model Open SWE uses when you don't specify one. You can
|
|
change this anytime in your settings.
|
|
</Dialog.Description>
|
|
<div className="flex flex-col gap-3">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<span className="text-xs font-medium">Default model</span>
|
|
<Select
|
|
value={modelId}
|
|
onValueChange={(v) => v && setModelId(v)}
|
|
>
|
|
<SelectTrigger className="w-48">
|
|
<SelectValue placeholder="Pick a model" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{options.data?.models.map((m) => (
|
|
<SelectItem key={m.id} value={m.id}>
|
|
{m.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="flex items-center justify-between gap-3">
|
|
<span className="text-xs font-medium">Reasoning effort</span>
|
|
<Select
|
|
value={effort}
|
|
onValueChange={(v) => v && setEffort(v)}
|
|
>
|
|
<SelectTrigger className="w-48">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{currentModel?.efforts.map((e) => (
|
|
<SelectItem key={e} value={e}>
|
|
{e}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
{error && <p className="text-xs text-destructive">{error}</p>}
|
|
<div className="mt-2 flex justify-end gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setDismissed(true)}
|
|
>
|
|
Maybe later
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
onClick={handleSaveModel}
|
|
disabled={!modelId || save.isPending}
|
|
>
|
|
{save.isPending
|
|
? "Saving…"
|
|
: needsSlack
|
|
? "Save & continue"
|
|
: "Save"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col gap-4">
|
|
<div className="flex items-center gap-3">
|
|
<IoLogoSlack className="size-6 shrink-0 text-muted-foreground" />
|
|
<Dialog.Title className="text-sm font-medium">
|
|
Connect your Slack account
|
|
</Dialog.Title>
|
|
</div>
|
|
<Dialog.Description className="text-xs text-muted-foreground">
|
|
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.
|
|
</Dialog.Description>
|
|
<div className="mt-2 flex justify-end gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setDismissed(true)}
|
|
>
|
|
Maybe later
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
onClick={() => window.location.assign(slackConnectUrl())}
|
|
>
|
|
<IoLogoSlack className="size-4" />
|
|
Connect Slack
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Dialog.Popup>
|
|
</Dialog.Portal>
|
|
</Dialog.Root>
|
|
)
|
|
}
|