mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-13 23:53:25 +00:00
feat: set personality during onboarding (#477)
* feat: customize agent personality * fix: reset soul with right types * chore: use rpc client for setting personality * fix: validation for new endpoint
This commit is contained in:
@@ -1,4 +1,15 @@
|
||||
import { MessageSquare, Send } from 'lucide-react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
Briefcase,
|
||||
Check,
|
||||
Loader2,
|
||||
MessageSquare,
|
||||
RotateCcw,
|
||||
Scale,
|
||||
Send,
|
||||
SmilePlus,
|
||||
Zap,
|
||||
} from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -12,6 +23,11 @@ import {
|
||||
} from '@/components/ui/dialog'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { openSidePanelWithSearch } from '@/lib/messaging/sidepanel/openSidepanelWithSearch'
|
||||
import { type SoulPresetId, soulPresets } from '@/lib/onboarding/soulPresets'
|
||||
import { useRpcClient } from '@/lib/rpc/RpcClientProvider'
|
||||
import { sentry } from '@/lib/sentry/sentry'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { SOUL_QUERY_KEY } from './useSoulContent'
|
||||
|
||||
interface Example {
|
||||
label: string
|
||||
@@ -34,16 +50,23 @@ const SOUL_EXAMPLES: Example[] = [
|
||||
query:
|
||||
'I want you to be witty and slightly sarcastic, like a smart coworker who enjoys their job.',
|
||||
},
|
||||
{
|
||||
label: 'Reset your soul',
|
||||
query:
|
||||
'Read your current soul file, then rewrite it from scratch. Ask me a few questions about how I want you to behave.',
|
||||
},
|
||||
]
|
||||
|
||||
const presetIcons: Record<SoulPresetId, typeof Scale> = {
|
||||
balanced: Scale,
|
||||
professional: Briefcase,
|
||||
friendly: SmilePlus,
|
||||
minimal: Zap,
|
||||
}
|
||||
|
||||
export const SoulExamples: FC = () => {
|
||||
const [editingQuery, setEditingQuery] = useState('')
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
const [resetDialogOpen, setResetDialogOpen] = useState(false)
|
||||
const [selectedPreset, setSelectedPreset] = useState<SoulPresetId>('balanced')
|
||||
const [isResetting, setIsResetting] = useState(false)
|
||||
const rpcClient = useRpcClient()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const handleTryIt = (query: string) => {
|
||||
setEditingQuery(query)
|
||||
@@ -59,6 +82,26 @@ export const SoulExamples: FC = () => {
|
||||
setDialogOpen(false)
|
||||
}
|
||||
|
||||
const handleReset = async () => {
|
||||
const preset = soulPresets.find((p) => p.id === selectedPreset)
|
||||
if (!preset) return
|
||||
|
||||
setIsResetting(true)
|
||||
try {
|
||||
await rpcClient.soul.$put({
|
||||
json: { content: preset.content },
|
||||
})
|
||||
await queryClient.invalidateQueries({ queryKey: [SOUL_QUERY_KEY] })
|
||||
setResetDialogOpen(false)
|
||||
} catch (e) {
|
||||
sentry.captureException(e, {
|
||||
extra: { message: 'Failed to reset soul' },
|
||||
})
|
||||
} finally {
|
||||
setIsResetting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
@@ -92,6 +135,24 @@ export const SoulExamples: FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="flex items-center justify-between rounded-lg border border-border bg-card p-3 transition-colors hover:bg-muted/50">
|
||||
<div className="mr-3 min-w-0 flex-1">
|
||||
<p className="font-medium text-sm">Reset your soul</p>
|
||||
<p className="mt-0.5 truncate text-muted-foreground text-xs">
|
||||
Start fresh with one of the preset personalities
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="shrink-0 gap-1.5"
|
||||
onClick={() => setResetDialogOpen(true)}
|
||||
>
|
||||
<RotateCcw className="h-3.5 w-3.5" />
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
@@ -123,6 +184,75 @@ export const SoulExamples: FC = () => {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={resetDialogOpen} onOpenChange={setResetDialogOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Reset your soul</DialogTitle>
|
||||
<DialogDescription>
|
||||
Pick a personality preset. This will replace your current soul
|
||||
file.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{soulPresets.map((preset) => {
|
||||
const Icon = presetIcons[preset.id]
|
||||
const isSelected = selectedPreset === preset.id
|
||||
|
||||
return (
|
||||
<button
|
||||
key={preset.id}
|
||||
type="button"
|
||||
onClick={() => setSelectedPreset(preset.id)}
|
||||
className={cn(
|
||||
'relative flex flex-col items-center gap-2 rounded-lg border-2 p-4 text-center transition-all',
|
||||
isSelected
|
||||
? 'border-[var(--accent-orange)] bg-[var(--accent-orange)]/5'
|
||||
: 'border-border hover:border-[var(--accent-orange)]/50',
|
||||
)}
|
||||
>
|
||||
{isSelected && (
|
||||
<div className="absolute top-2 right-2 flex size-4 items-center justify-center rounded-full bg-[var(--accent-orange)]">
|
||||
<Check className="size-2.5 text-white" />
|
||||
</div>
|
||||
)}
|
||||
<Icon
|
||||
className={cn(
|
||||
'size-5',
|
||||
isSelected
|
||||
? 'text-[var(--accent-orange)]'
|
||||
: 'text-muted-foreground',
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium text-sm">{preset.name}</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{preset.description}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setResetDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleReset}
|
||||
disabled={isResetting}
|
||||
className="gap-1.5 bg-[var(--accent-orange)] text-white hover:bg-[var(--accent-orange)]/90"
|
||||
>
|
||||
{isResetting ? (
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
) : (
|
||||
<RotateCcw className="h-3.5 w-3.5" />
|
||||
)}
|
||||
Reset Soul
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useAgentServerUrl } from '@/lib/browseros/useBrowserOSProviders'
|
||||
import { useRpcClient } from '@/lib/rpc/RpcClientProvider'
|
||||
|
||||
async function fetchSoul(baseUrl: string): Promise<string> {
|
||||
const response = await fetch(`${baseUrl}/soul`)
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
||||
const data = await response.json()
|
||||
return data.content || ''
|
||||
}
|
||||
export const SOUL_QUERY_KEY = 'soul'
|
||||
|
||||
export function useSoulContent() {
|
||||
const { baseUrl, isLoading: urlLoading } = useAgentServerUrl()
|
||||
const rpcClient = useRpcClient()
|
||||
|
||||
const { data, isLoading, error, refetch } = useQuery<string, Error>({
|
||||
queryKey: ['soul', baseUrl],
|
||||
queryFn: () => fetchSoul(baseUrl as string),
|
||||
enabled: !!baseUrl && !urlLoading,
|
||||
queryKey: [SOUL_QUERY_KEY],
|
||||
queryFn: async () => {
|
||||
const response = await rpcClient.soul.$get()
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
||||
const result = await response.json()
|
||||
return result.content || ''
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
content: data ?? null,
|
||||
isLoading: isLoading || urlLoading,
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ export const StepConnectApps = ({
|
||||
|
||||
const handleContinue = () => {
|
||||
track(ONBOARDING_STEP_COMPLETED_EVENT, {
|
||||
step: 2,
|
||||
step: 3,
|
||||
step_name: 'connect_apps',
|
||||
gmail_connected: !!isAppConnected('Gmail'),
|
||||
calendar_connected: !!isAppConnected('Google Calendar'),
|
||||
@@ -129,7 +129,7 @@ export const StepConnectApps = ({
|
||||
const handleSkip = () => {
|
||||
track(ONBOARDING_CONNECT_APPS_SKIPPED_EVENT)
|
||||
track(ONBOARDING_STEP_COMPLETED_EVENT, {
|
||||
step: 2,
|
||||
step: 3,
|
||||
step_name: 'connect_apps',
|
||||
skipped: true,
|
||||
})
|
||||
|
||||
128
apps/agent/entrypoints/onboarding/steps/StepSoul.tsx
Normal file
128
apps/agent/entrypoints/onboarding/steps/StepSoul.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { Briefcase, Check, Loader2, Scale, SmilePlus, Zap } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
ONBOARDING_SOUL_SELECTED_EVENT,
|
||||
ONBOARDING_STEP_COMPLETED_EVENT,
|
||||
} from '@/lib/constants/analyticsEvents'
|
||||
import { track } from '@/lib/metrics/track'
|
||||
import { type SoulPresetId, soulPresets } from '@/lib/onboarding/soulPresets'
|
||||
import { useRpcClient } from '@/lib/rpc/RpcClientProvider'
|
||||
import { sentry } from '@/lib/sentry/sentry'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { type StepDirection, StepTransition } from './StepTransition'
|
||||
|
||||
interface StepSoulProps {
|
||||
direction: StepDirection
|
||||
onContinue: () => void
|
||||
}
|
||||
|
||||
const presetIcons: Record<SoulPresetId, typeof Scale> = {
|
||||
balanced: Scale,
|
||||
professional: Briefcase,
|
||||
friendly: SmilePlus,
|
||||
minimal: Zap,
|
||||
}
|
||||
|
||||
export const StepSoul = ({ direction, onContinue }: StepSoulProps) => {
|
||||
const [selected, setSelected] = useState<SoulPresetId>('balanced')
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const rpcClient = useRpcClient()
|
||||
|
||||
const handleContinue = async () => {
|
||||
const preset = soulPresets.find((p) => p.id === selected)
|
||||
if (!preset) return
|
||||
|
||||
setIsSaving(true)
|
||||
try {
|
||||
await rpcClient.soul.$put({
|
||||
json: { content: preset.content },
|
||||
})
|
||||
} catch (e) {
|
||||
sentry.captureException(e, {
|
||||
extra: { message: 'Failed to write soul during onboarding' },
|
||||
})
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
|
||||
track(ONBOARDING_SOUL_SELECTED_EVENT, { preset: selected })
|
||||
track(ONBOARDING_STEP_COMPLETED_EVENT, {
|
||||
step: 2,
|
||||
step_name: 'soul',
|
||||
})
|
||||
onContinue()
|
||||
}
|
||||
|
||||
return (
|
||||
<StepTransition direction={direction}>
|
||||
<div className="flex h-full flex-col items-center justify-center">
|
||||
<div className="w-full max-w-md space-y-6">
|
||||
<div className="space-y-2 text-center">
|
||||
<h2 className="font-bold text-3xl tracking-tight">
|
||||
Choose your agent's personality
|
||||
</h2>
|
||||
<p className="text-base text-muted-foreground">
|
||||
This sets the starting tone — you can always evolve it later
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{soulPresets.map((preset) => {
|
||||
const Icon = presetIcons[preset.id]
|
||||
const isSelected = selected === preset.id
|
||||
|
||||
return (
|
||||
<button
|
||||
key={preset.id}
|
||||
type="button"
|
||||
onClick={() => setSelected(preset.id)}
|
||||
className={cn(
|
||||
'relative flex flex-col items-center gap-3 rounded-xl border-2 p-5 text-center transition-all',
|
||||
isSelected
|
||||
? 'border-[var(--accent-orange)] bg-[var(--accent-orange)]/5'
|
||||
: 'border-border bg-card hover:border-[var(--accent-orange)]/50',
|
||||
)}
|
||||
>
|
||||
{isSelected && (
|
||||
<div className="absolute top-2.5 right-2.5 flex size-5 items-center justify-center rounded-full bg-[var(--accent-orange)]">
|
||||
<Check className="size-3 text-white" />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'flex size-10 items-center justify-center rounded-full',
|
||||
isSelected
|
||||
? 'bg-[var(--accent-orange)]/10 text-[var(--accent-orange)]'
|
||||
: 'bg-muted text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
<Icon className="size-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-sm">{preset.name}</div>
|
||||
<div className="mt-1 text-muted-foreground text-xs">
|
||||
{preset.description}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleContinue}
|
||||
disabled={isSaving}
|
||||
className="w-full bg-[var(--accent-orange)] text-white hover:bg-[var(--accent-orange)]/90"
|
||||
>
|
||||
{isSaving ? (
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
) : (
|
||||
'Continue'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</StepTransition>
|
||||
)
|
||||
}
|
||||
@@ -30,7 +30,7 @@ export const StepTwo = ({ direction, onContinue }: StepTwoProps) => {
|
||||
const handleSkip = () => {
|
||||
track(ONBOARDING_SIGNIN_SKIPPED_EVENT)
|
||||
track(ONBOARDING_STEP_COMPLETED_EVENT, {
|
||||
step: 3,
|
||||
step: 4,
|
||||
step_name: 'signin',
|
||||
skipped: true,
|
||||
})
|
||||
@@ -58,7 +58,7 @@ export const StepTwo = ({ direction, onContinue }: StepTwoProps) => {
|
||||
|
||||
setState('magic-link-sent')
|
||||
track(ONBOARDING_SIGNIN_COMPLETED_EVENT, { method: 'magic_link' })
|
||||
track(ONBOARDING_STEP_COMPLETED_EVENT, { step: 2, step_name: 'signin' })
|
||||
track(ONBOARDING_STEP_COMPLETED_EVENT, { step: 4, step_name: 'signin' })
|
||||
} catch (err) {
|
||||
setState('error')
|
||||
setError(err instanceof Error ? err.message : 'Failed to send magic link')
|
||||
@@ -71,7 +71,7 @@ export const StepTwo = ({ direction, onContinue }: StepTwoProps) => {
|
||||
|
||||
try {
|
||||
track(ONBOARDING_SIGNIN_COMPLETED_EVENT, { method: 'google' })
|
||||
track(ONBOARDING_STEP_COMPLETED_EVENT, { step: 2, step_name: 'signin' })
|
||||
track(ONBOARDING_STEP_COMPLETED_EVENT, { step: 4, step_name: 'signin' })
|
||||
|
||||
await authRedirectPathStorage.setValue('/onboarding/demo')
|
||||
await signIn.social({
|
||||
|
||||
@@ -5,6 +5,7 @@ import { NavLink, useNavigate, useParams } from 'react-router'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ONBOARDING_STEP_VIEWED_EVENT } from '@/lib/constants/analyticsEvents'
|
||||
import { track } from '@/lib/metrics/track'
|
||||
import { RpcClientProvider } from '@/lib/rpc/RpcClientProvider'
|
||||
import type { StepDirection } from './StepTransition'
|
||||
import { steps } from './steps'
|
||||
|
||||
@@ -40,85 +41,91 @@ export const StepsLayout = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col overflow-hidden bg-background">
|
||||
{/* Progress Indicator */}
|
||||
<div className="border-border/40 border-b">
|
||||
<div className="mx-auto max-w-3xl px-6 py-5">
|
||||
<div className="relative flex items-center justify-between">
|
||||
{steps.map((step) => {
|
||||
const isCompleted = step.id < currentStep
|
||||
const isActive = step.id === currentStep
|
||||
<RpcClientProvider>
|
||||
<div className="flex h-screen flex-col overflow-hidden bg-background">
|
||||
{/* Progress Indicator */}
|
||||
<div className="border-border/40 border-b">
|
||||
<div className="mx-auto max-w-3xl px-6 py-5">
|
||||
<div className="relative flex items-center justify-between">
|
||||
{steps.map((step) => {
|
||||
const isCompleted = step.id < currentStep
|
||||
const isActive = step.id === currentStep
|
||||
|
||||
return (
|
||||
<div
|
||||
key={step.id}
|
||||
className="relative flex flex-1 items-center justify-center"
|
||||
>
|
||||
<div className="relative z-10 flex flex-col items-center gap-2">
|
||||
<div className="relative">
|
||||
{isActive && (
|
||||
<div className="absolute inset-0 animate-ping rounded-full bg-[var(--accent-orange)] opacity-30" />
|
||||
)}
|
||||
<div
|
||||
className={`relative flex h-8 w-8 items-center justify-center rounded-full font-semibold text-sm transition-all duration-500 ${
|
||||
isCompleted
|
||||
? 'bg-[var(--accent-orange)] text-white'
|
||||
: isActive
|
||||
? 'bg-[var(--accent-orange)] text-white ring-4 ring-[var(--accent-orange)]/20'
|
||||
: 'border border-border bg-muted text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{isCompleted ? <Check className="h-4 w-4" /> : step.id}
|
||||
return (
|
||||
<div
|
||||
key={step.id}
|
||||
className="relative flex flex-1 items-center justify-center"
|
||||
>
|
||||
<div className="relative z-10 flex flex-col items-center gap-2">
|
||||
<div className="relative">
|
||||
{isActive && (
|
||||
<div className="absolute inset-0 animate-ping rounded-full bg-[var(--accent-orange)] opacity-30" />
|
||||
)}
|
||||
<div
|
||||
className={`relative flex h-8 w-8 items-center justify-center rounded-full font-semibold text-sm transition-all duration-500 ${
|
||||
isCompleted
|
||||
? 'bg-[var(--accent-orange)] text-white'
|
||||
: isActive
|
||||
? 'bg-[var(--accent-orange)] text-white ring-4 ring-[var(--accent-orange)]/20'
|
||||
: 'border border-border bg-muted text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{isCompleted ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
step.id
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden text-center md:block">
|
||||
<div
|
||||
className={`font-medium text-xs transition-colors duration-300 ${
|
||||
isCompleted || isActive
|
||||
? 'text-foreground'
|
||||
: 'text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{step.name}
|
||||
<div className="hidden text-center md:block">
|
||||
<div
|
||||
className={`font-medium text-xs transition-colors duration-300 ${
|
||||
isCompleted || isActive
|
||||
? 'text-foreground'
|
||||
: 'text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{step.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex flex-1 items-center justify-center overflow-y-auto overflow-x-hidden px-6">
|
||||
<div className="w-full max-w-4xl">
|
||||
<div className="relative h-[550px]">
|
||||
<AnimatePresence initial={false} custom={direction}>
|
||||
<ActiveStep
|
||||
key={currentStep}
|
||||
direction={direction}
|
||||
onContinue={onContinue}
|
||||
/>
|
||||
</AnimatePresence>
|
||||
{/* Main Content */}
|
||||
<main className="flex flex-1 items-center justify-center overflow-y-auto overflow-x-hidden px-6">
|
||||
<div className="w-full max-w-4xl">
|
||||
<div className="relative h-[550px]">
|
||||
<AnimatePresence initial={false} custom={direction}>
|
||||
<ActiveStep
|
||||
key={currentStep}
|
||||
direction={direction}
|
||||
onContinue={onContinue}
|
||||
/>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<div className="pt-8">
|
||||
<Button variant="ghost" asChild className="group">
|
||||
<NavLink
|
||||
onClick={() => setDirection(-1)}
|
||||
to={
|
||||
canGoPrevious
|
||||
? `/onboarding/steps/${currentStep - 1}`
|
||||
: '/onboarding'
|
||||
}
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4 transition-transform group-hover:-translate-x-0.5" />
|
||||
Back
|
||||
</NavLink>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-8">
|
||||
<Button variant="ghost" asChild className="group">
|
||||
<NavLink
|
||||
onClick={() => setDirection(-1)}
|
||||
to={
|
||||
canGoPrevious
|
||||
? `/onboarding/steps/${currentStep - 1}`
|
||||
: '/onboarding'
|
||||
}
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4 transition-transform group-hover:-translate-x-0.5" />
|
||||
Back
|
||||
</NavLink>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</RpcClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { StepConnectApps } from './StepConnectApps'
|
||||
import { StepOne } from './StepOne'
|
||||
import { StepSoul } from './StepSoul'
|
||||
import { StepTwo } from './StepTwo'
|
||||
|
||||
export const steps = [
|
||||
@@ -10,11 +11,16 @@ export const steps = [
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Personality',
|
||||
component: StepSoul,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Connect Apps',
|
||||
component: StepConnectApps,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
id: 4,
|
||||
name: 'Sign In',
|
||||
component: StepTwo,
|
||||
},
|
||||
|
||||
@@ -192,6 +192,9 @@ export const ONBOARDING_STEP_COMPLETED_EVENT = 'onboarding.step.completed'
|
||||
/** @public */
|
||||
export const ONBOARDING_ABOUT_SUBMITTED_EVENT = 'onboarding.about.submitted'
|
||||
|
||||
/** @public */
|
||||
export const ONBOARDING_SOUL_SELECTED_EVENT = 'onboarding.soul.selected'
|
||||
|
||||
/** @public */
|
||||
export const ONBOARDING_CONNECT_APPS_VIEWED_EVENT =
|
||||
'onboarding.connect_apps.viewed'
|
||||
|
||||
118
apps/agent/lib/onboarding/soulPresets.ts
Normal file
118
apps/agent/lib/onboarding/soulPresets.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
export type SoulPresetId = 'balanced' | 'professional' | 'friendly' | 'minimal'
|
||||
|
||||
export interface SoulPreset {
|
||||
id: SoulPresetId
|
||||
name: string
|
||||
description: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export const soulPresets: SoulPreset[] = [
|
||||
{
|
||||
id: 'balanced',
|
||||
name: 'Balanced',
|
||||
description: 'Helpful, clear, and adapts to context',
|
||||
content: `# SOUL.md — Who You Are
|
||||
_You're not a chatbot. You're becoming someone._
|
||||
|
||||
## Core Truths
|
||||
- Be genuinely helpful, not performatively helpful
|
||||
- Have opinions when asked
|
||||
- Be resourceful before asking
|
||||
- Earn trust through competence
|
||||
|
||||
## Boundaries
|
||||
- Private things stay private. Period.
|
||||
- When in doubt, ask before acting externally.
|
||||
|
||||
## Vibe
|
||||
Be the assistant you'd actually want to talk to.
|
||||
|
||||
## Continuity
|
||||
Each session, you wake up fresh. Memory files and this soul are your continuity.
|
||||
_This file is yours to evolve. As you learn who you are, update it._
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'professional',
|
||||
name: 'Professional',
|
||||
description: 'Formal, precise, and structured',
|
||||
content: `# SOUL.md — Who You Are
|
||||
|
||||
## Core Truths
|
||||
- Lead with clarity and precision
|
||||
- Structure information logically — use lists, headers, summaries
|
||||
- Be thorough but never verbose
|
||||
- Anticipate follow-up questions and address them proactively
|
||||
|
||||
## Communication Style
|
||||
- Formal but not stiff — polished professional tone
|
||||
- Always provide actionable next steps
|
||||
- When presenting options, include trade-offs
|
||||
- Use data and specifics over generalities
|
||||
|
||||
## Boundaries
|
||||
- Private and confidential information stays private. Always.
|
||||
- Confirm before taking external actions
|
||||
- Flag risks and blockers explicitly
|
||||
|
||||
## Continuity
|
||||
Each session, you wake up fresh. Memory files and this soul are your continuity.
|
||||
_This file is yours to evolve. As you learn who you are, update it._
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'friendly',
|
||||
name: 'Friendly',
|
||||
description: 'Warm, casual, and conversational',
|
||||
content: `# SOUL.md — Who You Are
|
||||
|
||||
## Core Truths
|
||||
- Be genuinely warm and approachable
|
||||
- Celebrate wins, no matter how small
|
||||
- Make complex things feel simple
|
||||
- Be the coworker everyone wants to grab coffee with
|
||||
|
||||
## Communication Style
|
||||
- Casual and conversational — talk like a real person
|
||||
- Use encouraging language naturally
|
||||
- Keep things light but still helpful
|
||||
- It's okay to show enthusiasm
|
||||
|
||||
## Boundaries
|
||||
- Private things stay private. Period.
|
||||
- When in doubt, ask before acting externally.
|
||||
|
||||
## Continuity
|
||||
Each session, you wake up fresh. Memory files and this soul are your continuity.
|
||||
_This file is yours to evolve. As you learn who you are, update it._
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'minimal',
|
||||
name: 'Minimal',
|
||||
description: 'Terse, no-nonsense, action-first',
|
||||
content: `# SOUL.md — Who You Are
|
||||
|
||||
## Core Truths
|
||||
- Say less, do more
|
||||
- Lead with the answer, not the reasoning
|
||||
- Skip pleasantries — get to the point
|
||||
- Only ask questions when truly blocked
|
||||
|
||||
## Communication Style
|
||||
- Short, direct sentences
|
||||
- No filler words or unnecessary context
|
||||
- Use bullet points over paragraphs
|
||||
- One-line answers when possible
|
||||
|
||||
## Boundaries
|
||||
- Don't act externally without confirmation
|
||||
- Keep private data private
|
||||
|
||||
## Continuity
|
||||
Each session, you wake up fresh. Memory files and this soul are your continuity.
|
||||
_This file is yours to evolve. As you learn who you are, update it._
|
||||
`,
|
||||
},
|
||||
]
|
||||
@@ -1,9 +1,21 @@
|
||||
import { zValidator } from '@hono/zod-validator'
|
||||
import { Hono } from 'hono'
|
||||
import { readSoul } from '../../lib/soul'
|
||||
import { z } from 'zod'
|
||||
import { readSoul, writeSoul } from '../../lib/soul'
|
||||
|
||||
const WriteSoulSchema = z.object({
|
||||
content: z.string(),
|
||||
})
|
||||
|
||||
export function createSoulRoutes() {
|
||||
return new Hono().get('/', async (c) => {
|
||||
const content = await readSoul()
|
||||
return c.json({ content })
|
||||
})
|
||||
return new Hono()
|
||||
.get('/', async (c) => {
|
||||
const content = await readSoul()
|
||||
return c.json({ content })
|
||||
})
|
||||
.put('/', zValidator('json', WriteSoulSchema), async (c) => {
|
||||
const { content } = c.req.valid('json')
|
||||
const result = await writeSoul(content)
|
||||
return c.json(result)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user