-
Notifications
You must be signed in to change notification settings - Fork 39
[Feat] Guide first-admin setup through a conversational setup session #1823
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| 'use client'; | ||
|
|
||
| import { useEffect } from 'react'; | ||
| import { useMutation, useQuery } from '@tanstack/react-query'; | ||
|
|
||
| import { useUser } from '@/hooks/useUser'; | ||
| import { useTRPC } from '@/trpc/client'; | ||
| import type { FastSessionMessage } from '@/lib/server/fast-sessions'; | ||
|
|
||
| import { FastSessionTranscript } from '../../(sandbox)/sessions/[sessionId]/FastSessionTranscript'; | ||
| import { SetupRecommendationsInlineCard } from './SetupRecommendationsInlineCard'; | ||
| import { | ||
| SetupSourceControlPanelSurface, | ||
| useSetupRouteTransition, | ||
| useSetupSourceControlMilestoneEffect, | ||
| useSetupSourceControlStatus, | ||
| } from './SetupSourceControlPanel'; | ||
|
|
||
| /** | ||
| * Conversational setup workspace: the persisted Fast transcript is the | ||
| * primary surface, with the trusted source-control side panel (sheet on | ||
| * smaller screens) and inline automation recommendations around it. | ||
| */ | ||
| export function SetupConversationalSetup() { | ||
| const trpc = useTRPC(); | ||
| const { isSignedIn, user } = useUser(); | ||
| const isAdmin = user?.isAdmin === true; | ||
| const enabled = isSignedIn && isAdmin; | ||
|
|
||
| const statusQuery = useQuery( | ||
| trpc.setup.sessionStatus.queryOptions(undefined, { enabled }), | ||
| ); | ||
| const createSession = useMutation( | ||
| trpc.setup.getOrCreateSession.mutationOptions(), | ||
| ); | ||
|
|
||
| useEffect(() => { | ||
| if (enabled && statusQuery.data?.sessionId == null) { | ||
| createSession.mutate(); | ||
| } | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [enabled, statusQuery.data?.sessionId]); | ||
|
|
||
| const sessionId = statusQuery.data?.sessionId ?? null; | ||
| const setupCompleted = statusQuery.data?.completed ?? false; | ||
|
|
||
| useSetupRouteTransition({ sessionId, completed: setupCompleted }); | ||
|
|
||
| const { sourceControlSetup, connectedProviderCount } = | ||
| useSetupSourceControlStatus(enabled); | ||
| useSetupSourceControlMilestoneEffect({ | ||
| enabled: enabled && Boolean(sessionId), | ||
| connectedProviderCount, | ||
| }); | ||
| const messagesQuery = useQuery( | ||
| trpc.fastSessions.messages.queryOptions( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| { sessionId: sessionId! }, | ||
| { | ||
| enabled: Boolean(sessionId), | ||
| // The SSE stream in the transcript keeps rows fresh; this query only | ||
| // seeds the initial transcript snapshot. | ||
| staleTime: Infinity, | ||
| refetchOnWindowFocus: false, | ||
| }, | ||
| ), | ||
| ); | ||
| const sessionTasksQuery = useQuery( | ||
| trpc.fastSessions.tasks.queryOptions( | ||
| { sessionId: sessionId! }, | ||
| { enabled: Boolean(sessionId) }, | ||
| ), | ||
| ); | ||
|
|
||
| const sourceControlConnected = connectedProviderCount > 0; | ||
|
|
||
| if (!enabled) { | ||
| return null; | ||
| } | ||
|
|
||
| const sessionIdValue = sessionId; | ||
| const initialMessages = (messagesQuery.data?.messages ?? | ||
| []) as unknown as FastSessionMessage[]; | ||
| const hasOlderMessages = messagesQuery.data?.hasOlderMessages ?? false; | ||
| const timelineExtras = ( | ||
| <div className="space-y-4"> | ||
| {sessionTasksQuery.data && sessionTasksQuery.data.length > 0 ? ( | ||
| <div className="rounded-2xl border border-border bg-card p-4"> | ||
| <p className="mb-3 text-sm font-medium">Launched tasks</p> | ||
| <ul className="space-y-2"> | ||
| {sessionTasksQuery.data.map((task) => ( | ||
| <li key={task.taskId} className="text-sm"> | ||
| <a | ||
| className="underline decoration-border underline-offset-2 hover:decoration-foreground" | ||
| href={`/task/${task.taskId}`} | ||
| > | ||
| {task.title || task.taskId} | ||
| </a> | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| </div> | ||
| ) : null} | ||
| {sourceControlConnected ? <SetupRecommendationsInlineCard /> : null} | ||
| </div> | ||
| ); | ||
|
|
||
| return ( | ||
| // Break out of the centered setup column: the conversational workspace is | ||
| // a full-height two-pane surface, not a narrow wizard step. | ||
| <div className="relative left-[calc(-50vw+50%)] flex h-[calc(var(--effective-viewport-height)-8rem)] w-screen flex-col gap-4 px-2 lg:flex-row"> | ||
| <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden rounded-2xl border border-border bg-background"> | ||
| {sessionIdValue ? ( | ||
| <FastSessionTranscript | ||
| sessionId={sessionIdValue} | ||
| initialMessages={initialMessages} | ||
| hasOlderMessages={hasOlderMessages} | ||
| canReply | ||
| initialTitle="Set up Roomote." | ||
| fallbackTitle="Set up Roomote." | ||
| timelineExtras={timelineExtras} | ||
| /> | ||
| ) : ( | ||
| <div className="p-6 text-sm text-muted-foreground"> | ||
| Preparing your setup session… | ||
| </div> | ||
| )} | ||
| </div> | ||
| {sourceControlSetup && !setupCompleted ? ( | ||
| <div className="shrink-0 overflow-y-auto lg:w-[24rem]"> | ||
| <SetupSourceControlPanelSurface | ||
| sourceControlSetup={sourceControlSetup} | ||
| /> | ||
| </div> | ||
| ) : null} | ||
| </div> | ||
| ); | ||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| 'use client'; | ||
|
|
||
| import { useMutation } from '@tanstack/react-query'; | ||
|
|
||
| import { useUser } from '@/hooks/useUser'; | ||
| import { useTRPC } from '@/trpc/client'; | ||
|
|
||
| import { StepAutomationRecommendations } from './StepAutomationRecommendations'; | ||
|
|
||
| /** | ||
| * Inline automation-recommendations card. Rendered in the conversational | ||
| * setup workspace and in the setup session's normal route after activation. | ||
| * Apply or Skip notifies Roomote so it can acknowledge the choice and | ||
| * continue naturally. Optional: never blocks activation or launched tasks. | ||
| */ | ||
| export function SetupRecommendationsInlineCard() { | ||
| const trpc = useTRPC(); | ||
| const { user } = useUser(); | ||
| const notifyRecommendationChoice = useMutation( | ||
| trpc.setup.sessionMilestone.mutationOptions(), | ||
| ); | ||
|
|
||
| if (user?.isAdmin !== true) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <div className="rounded-2xl border border-border bg-card p-4"> | ||
| <p className="mb-3 text-sm font-medium">Recommended automations</p> | ||
| <StepAutomationRecommendations | ||
| onContinue={() => { | ||
| notifyRecommendationChoice.mutate({ | ||
| milestone: 'recommendations_notified', | ||
| eventType: 'recommendations_decided', | ||
| }); | ||
| }} | ||
| /> | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The mutation result is discarded and
sessionStatusis neither invalidated nor refetched. If its initial request observes no setup session (including the normal race where it completes before this mutation),sessionIdremainsnulland the page stays on "Preparing your setup session..." until a reload. Use the returned session ID or invalidate/refetchsetup.sessionStatusafter success; also avoid reissuing creation while the mutation is pending.