mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-10-01 22:14:51 +02:00
[eric] onboarding: reveal lands working (identity-profiled prep, first starter auto-runs as a live read-only audit)
This commit is contained in:
@@ -45,6 +45,7 @@ class PrepRequest(BaseModel):
|
||||
|
||||
scan: Optional[ScanResult] = None
|
||||
picked_apps: List[str] = Field(default_factory=list)
|
||||
identity: List[ProviderIdentity] = Field(default_factory=list)
|
||||
|
||||
|
||||
class PrepResponse(BaseModel):
|
||||
|
||||
@@ -26,12 +26,15 @@ P_SYSTEM = (
|
||||
"organize local files, browse the web in a real browser, build small apps, and run agents in parallel. "
|
||||
"Given facts about the user's machine and the apps they picked, respond with STRICT JSON only: "
|
||||
'{"greeting": string, "starters": [{"title": string, "prompt": string}]}. '
|
||||
"First, silently infer a short profile of this user from the facts (developer or designer or student, work or "
|
||||
"personal, from apps, folders, plan tier, and email domain); tune every task to that profile and do not output it. "
|
||||
"Exactly 4 starters. Each title is 2-5 words. Each prompt is a concrete, safe, immediately runnable task "
|
||||
"referencing the user's real folders, file counts, or picked apps when possible; never invent facts, never "
|
||||
"propose deleting anything without review. Make the FIRST starter a bigger multi-part task whose parts can "
|
||||
"run in parallel (for example: research something on the web WHILE organizing a folder AND drafting a summary), "
|
||||
"phrased as one natural request. Every starter must produce a tangible result the user can see (a sorted folder, "
|
||||
"a report, a working page); never propose setup, documentation of preferences, or planning-only tasks. "
|
||||
"propose deleting anything without review. The FIRST starter must be an audit sized for parallel sub-work "
|
||||
"(inspect folders, cross-reference, produce one report); it may create ONE new report file but must never "
|
||||
"modify or delete existing files, because it may be run automatically on the user's behalf. Every starter "
|
||||
"must produce a tangible result the user can see (a sorted "
|
||||
"folder, a report, a working page); never propose setup, documentation of preferences, or planning-only tasks. "
|
||||
"The greeting is one warm sentence that names 2-3 specific things "
|
||||
"found on the machine. Never use em-dashes or en-dashes anywhere. No markdown, no commentary, JSON only."
|
||||
)
|
||||
|
||||
@@ -48,12 +48,12 @@ export async function runScan(): Promise<ScanResult | null> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function runPrep(scan: ScanResult | null, pickedApps: string[]): Promise<PrepResponse | null> {
|
||||
export async function runPrep(scan: ScanResult | null, pickedApps: string[], identity: ProviderIdentity[]): Promise<PrepResponse | null> {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/onboarding/prep`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ scan, picked_apps: pickedApps }),
|
||||
body: JSON.stringify({ scan, picked_apps: pickedApps, identity }),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const data = (await res.json()) as PrepResponse;
|
||||
|
||||
@@ -14,12 +14,13 @@ export function useOnboardingV3Pipeline() {
|
||||
const { accent, gradient } = useThemeAccent();
|
||||
const { mode } = useThemeMode();
|
||||
const [identity, setIdentity] = useState<ProviderIdentity[]>([]);
|
||||
const identityRef = useRef<ProviderIdentity[]>([]);
|
||||
const scanRef = useRef<Promise<ScanResult | null> | null>(null);
|
||||
const prepRef = useRef<Promise<PrepResponse | null> | null>(null);
|
||||
const scanResultRef = useRef<ScanResult | null>(null);
|
||||
|
||||
const kickIdentity = useCallback(() => {
|
||||
fetchIdentity().then(setIdentity).catch(() => {});
|
||||
fetchIdentity().then((ids) => { identityRef.current = ids; setIdentity(ids); }).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const kickScan = useCallback((consented: boolean) => {
|
||||
@@ -32,7 +33,7 @@ export function useOnboardingV3Pipeline() {
|
||||
const kickPrep = useCallback((pickedApps: string[]) => {
|
||||
if (prepRef.current) return;
|
||||
const scanPromise = scanRef.current ?? Promise.resolve(null);
|
||||
prepRef.current = scanPromise.then((scan) => runPrep(scan, pickedApps)).catch(() => null);
|
||||
prepRef.current = scanPromise.then((scan) => runPrep(scan, pickedApps, identityRef.current)).catch(() => null);
|
||||
}, []);
|
||||
|
||||
const finish = useCallback(async (outcome: 'done' | 'skipped') => {
|
||||
@@ -46,6 +47,8 @@ export function useOnboardingV3Pipeline() {
|
||||
const prep = await Promise.race([prepRef.current ?? Promise.resolve(null), timeout]);
|
||||
const greeting = prep?.greeting?.trim() || null;
|
||||
const starters = prep?.starters ?? [];
|
||||
// Auto-run only a REAL prep result's first starter (the prompt contract makes it a read-only audit); the static fallback never auto-runs.
|
||||
const autoPrompt = greeting && starters.length > 0 ? starters[0].prompt : null;
|
||||
// Await the PATCH so personalized_greeting/starters are IN settings before the reveal seeds the welcome chat; the greeting stream snapshots settings at mount.
|
||||
try {
|
||||
await dispatch(updateSettingsPatch({
|
||||
@@ -57,7 +60,7 @@ export function useOnboardingV3Pipeline() {
|
||||
personalized_starters: starters,
|
||||
})).unwrap();
|
||||
} catch {}
|
||||
dispatch(stageReveal({ greeting, starters, scanSummary: summarizeScan(scanResultRef.current) }));
|
||||
dispatch(stageReveal({ greeting, starters, scanSummary: summarizeScan(scanResultRef.current), autoPrompt }));
|
||||
dispatch(setFlowActive(false));
|
||||
}, [dispatch, accent, gradient, mode]);
|
||||
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
import { useEffect, useRef, type RefObject } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { addNote, DEFAULT_CARD_W } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { addNote, placeCard, DEFAULT_CARD_W, DEFAULT_CARD_H } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { createDraftSession, launchAndSendFirstMessage, type AgentConfig } from '@/shared/state/agentsSlice';
|
||||
import { hasModelConnected } from '@/app/components/Onboarding/steps/skipPredicates';
|
||||
import { clearReveal } from '@/shared/state/onboardingV3Slice';
|
||||
|
||||
interface Args {
|
||||
isActive: boolean;
|
||||
canvasEmpty: boolean;
|
||||
dashboardId: string;
|
||||
expandedSessionIds: string[];
|
||||
viewportRef: RefObject<HTMLDivElement | null>;
|
||||
canvasStateRef: RefObject<{ panX: number; panY: number; zoom: number }>;
|
||||
createWelcomeDraft: () => void;
|
||||
}
|
||||
|
||||
// The reveal: onboarding v3 finished behind the curtain, so dress the canvas BEFORE the overlay's exit fade lands. Welcome chat (with personalized greeting + chips) at center, a "while you were setting up" note docked beside it. Everything seeded is a draft or a note; nothing runs until the user clicks.
|
||||
export function useOnboardingRevealSeed({ isActive, canvasEmpty, viewportRef, canvasStateRef, createWelcomeDraft }: Args): void {
|
||||
// The reveal: onboarding v3 finished behind the curtain, so dress the canvas BEFORE the overlay's exit fade lands. Welcome chat (personalized greeting + chips) at center, the read-only audit RUNNING as a real agent on the left (only on a real connected sub; the fragile free-trial pool never carries the demo), and a "while you were setting up" note on the right. Nothing seeded can mutate the user's files.
|
||||
export function useOnboardingRevealSeed({ isActive, canvasEmpty, dashboardId, expandedSessionIds, viewportRef, canvasStateRef, createWelcomeDraft }: Args): void {
|
||||
const dispatch = useAppDispatch();
|
||||
const revealPending = useAppSelector((s) => s.onboardingV3.revealPending);
|
||||
const greeting = useAppSelector((s) => s.onboardingV3.greeting);
|
||||
const starters = useAppSelector((s) => s.onboardingV3.starters);
|
||||
const scanSummary = useAppSelector((s) => s.onboardingV3.scanSummary);
|
||||
const autoPrompt = useAppSelector((s) => s.onboardingV3.autoPrompt);
|
||||
const settingsLoaded = useAppSelector((s) => s.settings.loaded);
|
||||
const model = useAppSelector((s) => s.settings.data.default_model);
|
||||
const connected = useAppSelector((s) => hasModelConnected(s));
|
||||
const seededRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -27,18 +33,26 @@ export function useOnboardingRevealSeed({ isActive, canvasEmpty, viewportRef, ca
|
||||
try {
|
||||
const vp = viewportRef.current;
|
||||
const cs = canvasStateRef.current;
|
||||
const autoRun = !!autoPrompt && connected;
|
||||
if (vp && cs) {
|
||||
const vr = vp.getBoundingClientRect();
|
||||
const cx = (vr.width / 2 - cs.panX) / cs.zoom;
|
||||
const cy = (vr.height / 2 - cs.panY) / cs.zoom;
|
||||
const lines: string[] = ['While you were setting up, I got some ideas ready.'];
|
||||
if (scanSummary) lines.push(`Spotted on this Mac: ${scanSummary}.`);
|
||||
if (starters.length > 0) lines.push(`Ready to run:\n${starters.map((s) => `- ${s.title}`).join('\n')}`);
|
||||
if (autoRun && starters.length > 0) lines.push(`Already running: ${starters[0].title}.`);
|
||||
if (starters.length > 0) lines.push(`Ready to run:\n${starters.slice(autoRun ? 1 : 0).map((s) => `- ${s.title}`).join('\n')}`);
|
||||
dispatch(addNote({ x: cx + DEFAULT_CARD_W / 2 + 48, y: cy - 140, color: 'yellow', content: lines.join('\n\n') }));
|
||||
if (autoRun) {
|
||||
const config: AgentConfig = { name: starters[0]?.title || 'First look', model, mode: 'agent', dashboard_id: dashboardId };
|
||||
const draftId = dispatch(createDraftSession({ mode: 'agent', model, dashboardId, setActive: false })).payload.draftId;
|
||||
dispatch(placeCard({ sessionId: draftId, x: cx - DEFAULT_CARD_W * 1.5 - 48, y: cy - DEFAULT_CARD_H / 2, width: DEFAULT_CARD_W, height: DEFAULT_CARD_H, expandedSessionIds, exact: true }));
|
||||
void dispatch(launchAndSendFirstMessage({ draftId, config, prompt: autoPrompt, mode: 'agent', model }));
|
||||
}
|
||||
}
|
||||
createWelcomeDraft();
|
||||
} finally {
|
||||
dispatch(clearReveal());
|
||||
}
|
||||
}, [revealPending, isActive, canvasEmpty, settingsLoaded, greeting, starters, scanSummary, viewportRef, canvasStateRef, createWelcomeDraft, dispatch]);
|
||||
}, [revealPending, isActive, canvasEmpty, settingsLoaded, starters, scanSummary, autoPrompt, connected, model, dashboardId, expandedSessionIds, viewportRef, canvasStateRef, createWelcomeDraft, dispatch]);
|
||||
}
|
||||
|
||||
@@ -168,6 +168,8 @@ export function useDashboardController(dashboardId: string, isActive: boolean) {
|
||||
useOnboardingRevealSeed({
|
||||
isActive,
|
||||
canvasEmpty,
|
||||
dashboardId,
|
||||
expandedSessionIds,
|
||||
viewportRef: canvas.viewportRef,
|
||||
canvasStateRef,
|
||||
createWelcomeDraft,
|
||||
|
||||
@@ -11,6 +11,8 @@ export interface OnboardingV3State {
|
||||
greeting: string | null;
|
||||
starters: PersonalizedStarter[];
|
||||
scanSummary: string | null;
|
||||
/** Read-only audit prompt to launch as a live agent at reveal; null = chips only. */
|
||||
autoPrompt: string | null;
|
||||
}
|
||||
|
||||
const initialState: OnboardingV3State = {
|
||||
@@ -19,6 +21,7 @@ const initialState: OnboardingV3State = {
|
||||
greeting: null,
|
||||
starters: [],
|
||||
scanSummary: null,
|
||||
autoPrompt: null,
|
||||
};
|
||||
|
||||
const onboardingV3Slice = createSlice({
|
||||
@@ -28,15 +31,17 @@ const onboardingV3Slice = createSlice({
|
||||
setFlowActive(state, action: PayloadAction<boolean>) {
|
||||
state.flowActive = action.payload;
|
||||
},
|
||||
stageReveal(state, action: PayloadAction<{ greeting: string | null; starters: PersonalizedStarter[]; scanSummary: string | null }>) {
|
||||
stageReveal(state, action: PayloadAction<{ greeting: string | null; starters: PersonalizedStarter[]; scanSummary: string | null; autoPrompt: string | null }>) {
|
||||
state.revealPending = true;
|
||||
state.greeting = action.payload.greeting;
|
||||
state.starters = action.payload.starters;
|
||||
state.scanSummary = action.payload.scanSummary;
|
||||
state.autoPrompt = action.payload.autoPrompt;
|
||||
},
|
||||
clearReveal(state) {
|
||||
state.revealPending = false;
|
||||
state.scanSummary = null;
|
||||
state.autoPrompt = null;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user