feat(ai): implement an AI chat window for agentic resume building (#3022)

This commit is contained in:
Amruth Pillai
2026-05-10 13:23:32 +02:00
committed by GitHub
parent 42e83cc676
commit 6787175a8a
91 changed files with 11894 additions and 135 deletions
@@ -0,0 +1,13 @@
import { create } from "zustand/react";
type BuilderAssistantStore = {
isOpen: boolean;
setOpen: (isOpen: boolean) => void;
toggleOpen: () => void;
};
export const useBuilderAssistantStore = create<BuilderAssistantStore>((set) => ({
isOpen: false,
setOpen: (isOpen) => set({ isOpen }),
toggleOpen: () => set((state) => ({ isOpen: !state.isOpen })),
}));
File diff suppressed because it is too large Load Diff
@@ -4,6 +4,7 @@ import { t } from "@lingui/core/macro";
import {
AlignCenterHorizontalIcon,
AlignTopIcon,
ChatCircleDotsIcon,
CircleNotchIcon,
CubeFocusIcon,
FileDocIcon,
@@ -26,6 +27,7 @@ import { cn } from "@reactive-resume/utils/style";
import { useCurrentResume } from "@/components/resume/builder-resume-draft";
import { authClient } from "@/libs/auth/client";
import { createResumePdfBlob } from "@/libs/resume/pdf-document";
import { useBuilderAssistantStore } from "./assistant-store";
type BuilderDockProps = {
pageLayout: BuilderPreviewPageLayout;
@@ -40,6 +42,8 @@ export function BuilderDock({ pageLayout, onTogglePageLayout }: BuilderDockProps
const { zoomIn, zoomOut, centerView } = useControls();
const [isPrinting, setIsPrinting] = useState(false);
const isAssistantOpen = useBuilderAssistantStore((state) => state.isOpen);
const toggleAssistant = useBuilderAssistantStore((state) => state.toggleOpen);
const publicUrl = useMemo(() => {
if (!session?.user.username || !resume?.slug) return "";
@@ -108,6 +112,12 @@ export function BuilderDock({ pageLayout, onTogglePageLayout }: BuilderDockProps
title={t`Toggle page stacking`}
onClick={onTogglePageLayout}
/>
<DockIcon
icon={ChatCircleDotsIcon}
title={isAssistantOpen ? t`Close AI assistant` : t`Open AI assistant`}
onClick={toggleAssistant}
active={isAssistantOpen}
/>
<div className="mx-1 h-8 w-px bg-border" />
<DockIcon icon={LinkSimpleIcon} title={t`Copy URL`} onClick={() => onCopyUrl()} />
<DockIcon icon={FileJsIcon} title={t`Download JSON`} onClick={() => onDownloadJSON()} />
@@ -130,9 +140,10 @@ type DockIconProps = {
disabled?: boolean;
onClick: () => void;
iconClassName?: string;
active?: boolean;
};
function DockIcon({ icon: Icon, title, disabled, onClick, iconClassName }: DockIconProps) {
function DockIcon({ icon: Icon, title, disabled, onClick, iconClassName, active }: DockIconProps) {
return (
<Tooltip>
<TooltipTrigger
@@ -143,7 +154,14 @@ function DockIcon({ icon: Icon, title, disabled, onClick, iconClassName }: DockI
whileTap={disabled ? undefined : { scale: 0.97 }}
transition={{ duration: 0.15, ease: "easeOut" }}
>
<Button size="icon" variant="ghost" disabled={disabled} onClick={onClick}>
<Button
size="icon"
variant="ghost"
disabled={disabled}
className={cn(active && "bg-primary/10 text-primary hover:bg-primary/15 hover:text-primary")}
onClick={onClick}
aria-label={title}
>
<Icon className={cn("size-4", iconClassName)} />
</Button>
</motion.div>
@@ -5,6 +5,7 @@ import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/
import { Input } from "@reactive-resume/ui/components/input";
import { URLInput } from "@/components/input/url-input";
import { useCurrentBuilderResumeSelector, useUpdateResumeData } from "@/components/resume/builder-resume-draft";
import { useSyncFormValues } from "@/hooks/use-sync-form-values";
import { useAppForm } from "@/libs/tanstack-form";
import { SectionBase } from "../shared/section-base";
import { CustomFieldsSection } from "./custom-fields";
@@ -38,6 +39,7 @@ function BasicsSectionForm() {
persist(value);
},
});
useSyncFormValues(form, basics);
return (
<form
@@ -18,6 +18,7 @@ import {
} from "@reactive-resume/ui/components/input-group";
import { ColorPicker } from "@/components/input/color-picker";
import { useCurrentResume, useUpdateResumeData } from "@/components/resume/builder-resume-draft";
import { useSyncFormValues } from "@/hooks/use-sync-form-values";
import { getReadableErrorMessage } from "@/libs/error-message";
import { orpc } from "@/libs/orpc/client";
import { useAppForm } from "@/libs/tanstack-form";
@@ -73,6 +74,7 @@ function PictureSectionForm() {
persist(value);
},
});
useSyncFormValues(form, picture);
const handleAutoSave = () => {
persist(form.state.values);
@@ -12,6 +12,7 @@ import { IconPicker } from "@/components/input/icon-picker";
import { LevelTypeCombobox } from "@/components/level/combobox";
import { LevelDisplay } from "@/components/level/display";
import { useCurrentResume, useUpdateResumeData } from "@/components/resume/builder-resume-draft";
import { useSyncFormValues } from "@/hooks/use-sync-form-values";
import { useAppForm } from "@/libs/tanstack-form";
import { SectionBase } from "../shared/section-base";
@@ -45,6 +46,7 @@ function ColorSectionForm() {
persist(value);
},
});
useSyncFormValues(form, colors);
const handleAutoSave = () => {
persist(form.state.values);
@@ -266,6 +268,7 @@ function LevelSectionForm() {
persist(value);
},
});
useSyncFormValues(form, levelDesign);
const handleAutoSave = () => {
persist(form.state.values);
@@ -10,6 +10,7 @@ import {
} from "@reactive-resume/ui/components/input-group";
import { Slider } from "@reactive-resume/ui/components/slider";
import { useCurrentResume, useUpdateResumeData } from "@/components/resume/builder-resume-draft";
import { useSyncFormValues } from "@/hooks/use-sync-form-values";
import { useAppForm } from "@/libs/tanstack-form";
import { SectionBase } from "../../shared/section-base";
import { LayoutPages } from "./pages";
@@ -45,6 +46,7 @@ function LayoutSectionForm() {
persist(value);
},
});
useSyncFormValues(form, { sidebarWidth: layout.sidebarWidth });
const handleAutoSave = () => {
persist(form.state.values);
@@ -13,6 +13,7 @@ import { Switch } from "@reactive-resume/ui/components/switch";
import { getLocaleOptions } from "@/components/locale/combobox";
import { useResume, useUpdateResumeData } from "@/components/resume/builder-resume-draft";
import { Combobox } from "@/components/ui/combobox";
import { useSyncFormValues } from "@/hooks/use-sync-form-values";
import { useAppForm } from "@/libs/tanstack-form";
import { SectionBase } from "../shared/section-base";
@@ -46,6 +47,7 @@ function PageSectionForm() {
persist(value);
},
});
useSyncFormValues(form, page);
const handleAutoSave = <K extends keyof FormValues>(name: K, value: FormValues[K]) => {
persist({ ...form.state.values, [name]: value });
@@ -12,6 +12,7 @@ import {
import { Separator } from "@reactive-resume/ui/components/separator";
import { useResume, useUpdateResumeData } from "@/components/resume/builder-resume-draft";
import { FontFamilyCombobox, FontWeightCombobox, getNextWeights } from "@/components/typography/combobox";
import { useSyncFormValues } from "@/hooks/use-sync-form-values";
import { useAppForm } from "@/libs/tanstack-form";
import { SectionBase } from "../shared/section-base";
@@ -47,6 +48,7 @@ function TypographySectionForm() {
persist(value);
},
});
useSyncFormValues(form, typography);
const handleAutoSave = () => {
persist(form.state.values);
@@ -13,9 +13,11 @@ import {
useMergeResumeMetadata,
useResumeCleanup,
useResumeStore,
useResumeUpdateSubscription,
} from "@/components/resume/builder-resume-draft";
import { useIsMobile } from "@/hooks/use-mobile";
import { orpc } from "@/libs/orpc/client";
import { BuilderAssistant } from "./-components/assistant";
import { BuilderHeader } from "./-components/header";
import { BuilderSidebarLeft } from "./-sidebar/left";
import { BuilderSidebarRight } from "./-sidebar/right";
@@ -59,6 +61,7 @@ function RouteComponent() {
const isInitialized = isReady && initializedResumeId === resumeId;
useResumeCleanup();
useResumeUpdateSubscription();
useEffect(() => {
if (isInitialized) return;
@@ -76,6 +79,7 @@ function RouteComponent() {
resume.isLocked,
resume.isPublic,
resume.hasPassword,
resume.updatedAt,
resume,
]);
@@ -164,6 +168,8 @@ function BuilderLayoutShell({ initialLayout }: BuilderLayoutShellProps) {
<BuilderSidebarRight />
</ResizablePanel>
</ResizableGroup>
<BuilderAssistant />
</div>
);
}