chore: update dependencies

This commit is contained in:
Amruth Pillai
2026-07-04 19:06:31 +02:00
parent 44fa2badb4
commit d87c6758ab
33 changed files with 1199 additions and 625 deletions
@@ -1,32 +1,19 @@
// @vitest-environment happy-dom
import { render, screen } from "@testing-library/react";
import { beforeAll, describe, expect, it } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { describe, expect, it } from "vitest";
import { LoadingScreen } from "./loading-screen";
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
describe("LoadingScreen", () => {
it("renders a spinner and the loading text", () => {
render(
<I18nProvider i18n={i18n}>
<LoadingScreen />
</I18nProvider>,
);
it("renders the Reactive Resume logo and spinner", () => {
render(<LoadingScreen />);
expect(screen.getByText("Loading…")).toBeInTheDocument();
expect(screen.getAllByAltText("Reactive Resume")).toHaveLength(2);
expect(screen.getByLabelText("Loading")).toBeInTheDocument();
});
it("fills the viewport (fixed inset-0)", () => {
const { container } = render(
<I18nProvider i18n={i18n}>
<LoadingScreen />
</I18nProvider>,
);
const { container } = render(<LoadingScreen />);
const wrapper = container.firstChild as HTMLElement;
expect(wrapper.className).toContain("fixed");
@@ -1,13 +1,11 @@
import { Trans } from "@lingui/react/macro";
import { BrandIcon } from "@reactive-resume/ui/components/brand-icon";
import { Spinner } from "@reactive-resume/ui/components/spinner";
export function LoadingScreen() {
return (
<div className="fixed inset-0 z-50 flex h-svh w-svw items-center justify-center gap-x-3 bg-background">
<div className="fixed inset-0 z-50 flex h-svh w-svw flex-col items-center justify-center gap-y-6 bg-background">
<BrandIcon variant="logo" className="size-12" />
<Spinner className="size-6" />
<p className="text-muted-foreground">
<Trans>Loading</Trans>
</p>
</div>
);
}
+2 -7
View File
@@ -23,6 +23,7 @@ import { Tabs, TabsList, TabsTrigger } from "@reactive-resume/ui/components/tabs
import { Tooltip, TooltipContent, TooltipTrigger } from "@reactive-resume/ui/components/tooltip";
import { downloadWithAnchor, generateFilename } from "@reactive-resume/utils/file";
import { cn } from "@reactive-resume/utils/style";
import { LoadingScreen } from "@/components/layout/loading-screen";
import { createResumePdfBlob } from "@/features/resume/export/pdf-document";
import { ResumePreview } from "@/features/resume/preview/preview";
import { orpc } from "@/libs/orpc/client";
@@ -242,13 +243,7 @@ function RouteComponent() {
}
}, []);
if (isLoading) {
return (
<div className="grid h-svh place-items-center bg-background text-muted-foreground">
<Trans>Loading agent workspace</Trans>
</div>
);
}
if (isLoading) return <LoadingScreen />;
if (error || !data) {
return (
@@ -28,7 +28,14 @@ import { m } from "motion/react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import ReactMarkdown from "react-markdown";
import { toast } from "sonner";
import { Badge } from "@reactive-resume/ui/components/badge";
import {
Attachment,
AttachmentContent,
AttachmentGroup,
AttachmentMedia,
AttachmentTitle,
} from "@reactive-resume/ui/components/attachment";
import { Bubble, BubbleContent } from "@reactive-resume/ui/components/bubble";
import { Button } from "@reactive-resume/ui/components/button";
import {
DropdownMenu,
@@ -37,7 +44,16 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@reactive-resume/ui/components/dropdown-menu";
import { ScrollArea } from "@reactive-resume/ui/components/scroll-area";
import { Marker, MarkerContent, MarkerIcon } from "@reactive-resume/ui/components/marker";
import { Message, MessageContent } from "@reactive-resume/ui/components/message";
import {
MessageScroller,
MessageScrollerButton,
MessageScrollerContent,
MessageScrollerItem,
MessageScrollerProvider,
MessageScrollerViewport,
} from "@reactive-resume/ui/components/message-scroller";
import { Textarea } from "@reactive-resume/ui/components/textarea";
import { cn } from "@reactive-resume/utils/style";
import { useConfirm } from "@/hooks/use-confirm";
@@ -65,6 +81,12 @@ type AssistantMarkdownProps = {
text: string;
};
type FileAttachmentProps = {
filename?: string | null;
mediaType?: string | null;
state?: "idle" | "uploading" | "processing" | "error" | "done";
};
type MessagePartProps = {
part: UIMessage["parts"][number];
isUser: boolean;
@@ -392,23 +414,47 @@ function AssistantMarkdown({ text }: AssistantMarkdownProps) {
);
}
function FileAttachment({ filename, mediaType, state = "done" }: FileAttachmentProps) {
return (
<Attachment size="sm" state={state}>
<AttachmentMedia>
<FileIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>{filename || t`Attachment`}</AttachmentTitle>
{mediaType ? <span className="sr-only">{mediaType}</span> : null}
</AttachmentContent>
</Attachment>
);
}
function MessagePart({ part, isUser, onAnswer, onRevert, isReverting, actionsById }: MessagePartProps) {
if (part.type === "text") {
return isUser ? (
<div className="whitespace-pre-wrap leading-relaxed">{part.text}</div>
) : (
<AssistantMarkdown text={part.text} />
return (
<Bubble variant={isUser ? "default" : "ghost"} align={isUser ? "end" : "start"}>
<BubbleContent>
{isUser ? (
<div className="whitespace-pre-wrap leading-relaxed">{part.text}</div>
) : (
<AssistantMarkdown text={part.text} />
)}
</BubbleContent>
</Bubble>
);
}
if (part.type === "reasoning") {
return (
<details className="rounded-md border bg-muted/40 px-3 py-2 text-sm">
<summary className="cursor-pointer text-muted-foreground">
<Trans>Thinking</Trans>
</summary>
<div className="mt-2 whitespace-pre-wrap">{part.text}</div>
</details>
<Bubble variant="outline" className="max-w-full">
<BubbleContent className="w-full">
<details className="text-sm">
<summary className="cursor-pointer text-muted-foreground">
<Trans>Thinking</Trans>
</summary>
<div className="mt-2 whitespace-pre-wrap">{part.text}</div>
</details>
</BubbleContent>
</Bubble>
);
}
@@ -421,16 +467,20 @@ function MessagePart({ part, isUser, onAnswer, onRevert, isReverting, actionsByI
const question = typeof input.question === "string" ? input.question : t`The agent needs your input.`;
return (
<div className="space-y-3 rounded-md border bg-card p-3">
<div className="font-medium">{question}</div>
<div className="flex flex-wrap gap-2">
{choices.map((choice) => (
<Button key={choice} size="sm" variant="outline" onClick={() => onAnswer(part.toolCallId, choice)}>
{choice}
</Button>
))}
</div>
</div>
<Bubble variant="outline" className="max-w-full">
<BubbleContent className="w-full">
<div className="space-y-3">
<div className="font-medium">{question}</div>
<div className="flex flex-wrap gap-2">
{choices.map((choice) => (
<Button key={choice} size="sm" variant="outline" onClick={() => onAnswer(part.toolCallId, choice)}>
{choice}
</Button>
))}
</div>
</div>
</BubbleContent>
</Bubble>
);
}
@@ -442,33 +492,38 @@ function MessagePart({ part, isUser, onAnswer, onRevert, isReverting, actionsByI
const actionId = typeof output?.actionId === "string" ? output.actionId : null;
const action = actionId ? actionsById.get(actionId) : undefined;
return <PatchToolCard part={part} action={action} onRevert={onRevert} isReverting={isReverting} />;
return (
<Bubble variant="outline" className="max-w-full">
<BubbleContent className="w-full">
<PatchToolCard part={part} action={action} onRevert={onRevert} isReverting={isReverting} />
</BubbleContent>
</Bubble>
);
}
if (part.type === "source-url") {
const title = part.title?.trim() || null;
return (
<a className="block text-primary text-sm underline" href={part.url} target="_blank" rel="noreferrer">
{title ? (
<>
<span className="block truncate">{title}</span>
<span className="block truncate text-muted-foreground">{part.url}</span>
</>
) : (
<span className="block truncate">{part.url}</span>
)}
</a>
<Bubble variant="ghost" className="max-w-full">
<BubbleContent className="w-full">
<a className="block text-primary text-sm underline" href={part.url} target="_blank" rel="noreferrer">
{title ? (
<>
<span className="block truncate">{title}</span>
<span className="block truncate text-muted-foreground">{part.url}</span>
</>
) : (
<span className="block truncate">{part.url}</span>
)}
</a>
</BubbleContent>
</Bubble>
);
}
if (part.type === "file") {
return (
<div className="flex max-w-full items-center gap-2 rounded-md border bg-background/20 px-2 py-1 text-sm">
<FileIcon className="shrink-0" />
<span className="truncate">{part.filename ?? part.url}</span>
</div>
);
return <FileAttachment filename={part.filename ?? part.url} mediaType={part.mediaType} />;
}
return null;
@@ -478,15 +533,8 @@ function ChatMessage({ message, onAnswer, onRevert, isReverting, actionsById }:
const isUser = message.role === "user";
return (
<div className={cn("flex", isUser ? "justify-end" : "justify-start")}>
<div
className={cn(
"space-y-3 text-sm",
isUser
? "max-w-[86%] rounded-md bg-primary px-4 py-3 text-primary-foreground"
: "w-full max-w-full py-1 text-foreground",
)}
>
<Message align={isUser ? "end" : "start"}>
<MessageContent className={cn(isUser ? "items-end" : "items-start")}>
{message.parts.map((part) => (
<MessagePart
key={getMessagePartKey(message.id, part)}
@@ -498,8 +546,8 @@ function ChatMessage({ message, onAnswer, onRevert, isReverting, actionsById }:
actionsById={actionsById}
/>
))}
</div>
</div>
</MessageContent>
</Message>
);
}
@@ -822,50 +870,63 @@ function AgentChatMessages({
onStarterSelect,
}: AgentChatMessagesProps) {
return (
<ScrollArea className="min-h-0 flex-1">
<div className="mx-auto flex max-w-3xl flex-col gap-4 p-4">
{messages.length === 0 ? (
<div className="grid gap-6 py-12 text-center">
<SparkleIcon className="mx-auto size-8 text-muted-foreground" />
<h2 className="font-semibold text-2xl">
<Trans>What do you want to do?</Trans>
</h2>
<StarterPromptMarquee onSelect={onStarterSelect} />
</div>
) : null}
{messages.map((message) => (
<ChatMessage
key={message.id}
message={message}
isReverting={isReverting}
actionsById={actionsById}
onAnswer={onAnswer}
onRevert={onRevert}
/>
))}
{isStreaming ? (
<div className="flex justify-start">
<div className="rounded-md bg-muted px-4 py-3 text-muted-foreground text-sm">
<Trans>Working</Trans>
</div>
</div>
) : null}
{error ? (
<div className="flex items-center justify-between gap-3 rounded-md border border-rose-300 bg-rose-50 p-3 text-rose-950 text-sm dark:bg-rose-950/20 dark:text-rose-200">
<span>{error.message}</span>
{!isReadOnly ? (
<Button size="sm" variant="outline" type="button" onClick={onRetry}>
<ArrowClockwiseIcon />
<Trans>Retry</Trans>
</Button>
<MessageScrollerProvider autoScroll defaultScrollPosition="end">
<MessageScroller className="min-h-0 flex-1">
<MessageScrollerViewport>
<MessageScrollerContent className="mx-auto flex w-full max-w-3xl flex-col gap-4 p-4">
{messages.length === 0 ? (
<div className="grid gap-6 py-12 text-center">
<SparkleIcon className="mx-auto size-8 text-muted-foreground" />
<h2 className="font-semibold text-2xl">
<Trans>What do you want to do?</Trans>
</h2>
<StarterPromptMarquee onSelect={onStarterSelect} />
</div>
) : null}
</div>
) : null}
</div>
</ScrollArea>
{messages.map((message) => (
<MessageScrollerItem key={message.id} messageId={message.id} scrollAnchor={message.role === "user"}>
<ChatMessage
message={message}
isReverting={isReverting}
actionsById={actionsById}
onAnswer={onAnswer}
onRevert={onRevert}
/>
</MessageScrollerItem>
))}
{isStreaming ? (
<MessageScrollerItem>
<Marker className="w-fit rounded-md bg-muted px-4 py-3">
<MarkerIcon>
<SparkleIcon />
</MarkerIcon>
<MarkerContent>
<Trans>Working</Trans>
</MarkerContent>
</Marker>
</MessageScrollerItem>
) : null}
{error ? (
<MessageScrollerItem>
<Marker className="items-center justify-between gap-3 rounded-md border border-rose-300 bg-rose-50 p-3 text-rose-950 dark:bg-rose-950/20 dark:text-rose-200">
<MarkerContent>{error.message}</MarkerContent>
{!isReadOnly ? (
<Button size="sm" variant="outline" type="button" onClick={onRetry}>
<ArrowClockwiseIcon />
<Trans>Retry</Trans>
</Button>
) : null}
</Marker>
</MessageScrollerItem>
) : null}
</MessageScrollerContent>
</MessageScrollerViewport>
<MessageScrollerButton />
</MessageScroller>
</MessageScrollerProvider>
);
}
@@ -977,16 +1038,18 @@ function AgentChatComposer({
onSend();
}}
>
<div className="mx-auto max-w-3xl space-y-2">
<div className="mx-auto flex max-w-3xl flex-col gap-2">
{pendingAttachments.length > 0 ? (
<div className="flex flex-wrap gap-2">
<AttachmentGroup className="flex-wrap gap-2 overflow-visible py-0">
{pendingAttachments.map((attachment) => (
<Badge key={attachment.id} variant="secondary">
<FileIcon />
{attachment.filename}
</Badge>
<FileAttachment
key={attachment.id}
filename={attachment.filename}
mediaType={attachment.mediaType}
state={isUploading ? "uploading" : "done"}
/>
))}
</div>
</AttachmentGroup>
) : null}
<div className="flex items-end gap-1 rounded-md border bg-card p-1.5">