mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-23 23:02:17 +10:00
feat(ai): implement an AI chat window for agentic resume building (#3022)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user