Add frontend regression test suite
Browse files- .github/workflows/ci.yml +6 -0
- .gitignore +1 -0
- frontend/components/chat-message.test.tsx +234 -0
- frontend/components/chat-message.tsx +3 -3
- frontend/components/chat-shell.test.tsx +556 -0
- frontend/components/chat-shell.tsx +26 -5
- frontend/components/markdown-block.test.tsx +78 -0
- frontend/components/source-sidebar.test.tsx +119 -0
- frontend/components/source-sidebar.tsx +5 -1
- frontend/e2e/chat-flow.spec.ts +202 -0
- frontend/lib/api.test.mts +111 -0
- frontend/lib/chat-http-transport.test.mts +130 -0
- frontend/lib/chat-sdk-lifecycle.test.mts +118 -0
- frontend/lib/chat-stream.test.mts +162 -0
- frontend/lib/chat-ui.test.mts +284 -0
- frontend/lib/chat-ui.ts +8 -1
- frontend/package-lock.json +1799 -128
- frontend/package.json +12 -2
- frontend/playwright.config.ts +27 -0
- frontend/test/setup.ts +13 -0
- frontend/vitest.config.ts +16 -0
.github/workflows/ci.yml
CHANGED
|
@@ -93,3 +93,9 @@ jobs:
|
|
| 93 |
|
| 94 |
- name: Build frontend
|
| 95 |
run: npm run build
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
- name: Build frontend
|
| 95 |
run: npm run build
|
| 96 |
+
|
| 97 |
+
- name: Install Chromium
|
| 98 |
+
run: npx playwright install --with-deps chromium
|
| 99 |
+
|
| 100 |
+
- name: Run browser tests
|
| 101 |
+
run: npm run test:e2e
|
.gitignore
CHANGED
|
@@ -251,3 +251,4 @@ workshop_AIE/
|
|
| 251 |
|
| 252 |
# Local course-content working data (private Thinkific curriculum mapping + markdown) — never commit
|
| 253 |
/full_stack_course/
|
|
|
|
|
|
| 251 |
|
| 252 |
# Local course-content working data (private Thinkific curriculum mapping + markdown) — never commit
|
| 253 |
/full_stack_course/
|
| 254 |
+
/output/playwright/
|
frontend/components/chat-message.test.tsx
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { cleanup, fireEvent, render, screen, within } from "@testing-library/react";
|
| 2 |
+
import { afterEach, describe, expect, it } from "vitest";
|
| 3 |
+
|
| 4 |
+
import { ChatMessage } from "@/components/chat-message";
|
| 5 |
+
import type { TutorMessage, TutorMessagePart } from "@/lib/chat-ui";
|
| 6 |
+
|
| 7 |
+
afterEach(cleanup);
|
| 8 |
+
|
| 9 |
+
function assistantMessage(
|
| 10 |
+
parts: TutorMessagePart[],
|
| 11 |
+
id = "assistant-message",
|
| 12 |
+
): TutorMessage {
|
| 13 |
+
return { id, role: "assistant", parts } as unknown as TutorMessage;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
describe("ChatMessage activity rendering", () => {
|
| 17 |
+
it("preserves reasoning, tool, reasoning order and counts merged thoughts", () => {
|
| 18 |
+
render(
|
| 19 |
+
<ChatMessage
|
| 20 |
+
showAssistantActions={false}
|
| 21 |
+
message={assistantMessage([
|
| 22 |
+
{ type: "reasoning", text: "First thought" },
|
| 23 |
+
{ type: "reasoning", text: "Continuation of the first thought" },
|
| 24 |
+
{
|
| 25 |
+
type: "tool-retrieve_tutor_context",
|
| 26 |
+
toolCallId: "retrieve-1",
|
| 27 |
+
state: "output-available",
|
| 28 |
+
input: { query: "agent memory" },
|
| 29 |
+
output: {
|
| 30 |
+
matches: [
|
| 31 |
+
{ docId: "doc-a", url: "https://example.com/a" },
|
| 32 |
+
{ docId: "doc-b", url: "https://example.com/b" },
|
| 33 |
+
// Repeated evidence must not inflate the source count.
|
| 34 |
+
{ docId: "doc-a", url: "https://example.com/a#section" },
|
| 35 |
+
],
|
| 36 |
+
},
|
| 37 |
+
},
|
| 38 |
+
{ type: "reasoning", text: "Second thought after retrieval" },
|
| 39 |
+
{ type: "text", text: "Final answer" },
|
| 40 |
+
])}
|
| 41 |
+
/>,
|
| 42 |
+
);
|
| 43 |
+
|
| 44 |
+
const activityToggle = screen.getByRole("button", {
|
| 45 |
+
name: "Activity · 1 tool · 2 thoughts · 2 sources",
|
| 46 |
+
});
|
| 47 |
+
expect(activityToggle.getAttribute("aria-expanded")).toBe("false");
|
| 48 |
+
|
| 49 |
+
fireEvent.click(activityToggle);
|
| 50 |
+
|
| 51 |
+
const items = within(screen.getByRole("list")).getAllByRole("listitem");
|
| 52 |
+
expect(items).toHaveLength(3);
|
| 53 |
+
expect(items[0]?.textContent).toContain("First thought");
|
| 54 |
+
expect(items[0]?.textContent).toContain("Continuation of the first thought");
|
| 55 |
+
expect(items[1]?.textContent).toContain("Hybrid search");
|
| 56 |
+
expect(items[1]?.textContent).toContain("agent memory");
|
| 57 |
+
expect(items[2]?.textContent).toContain("Second thought after retrieval");
|
| 58 |
+
expect(screen.getByText("Final answer")).toBeTruthy();
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
it("shows Running only while a tool call is genuinely unfinished", () => {
|
| 62 |
+
const { rerender } = render(
|
| 63 |
+
<ChatMessage
|
| 64 |
+
isStreaming
|
| 65 |
+
showAssistantActions={false}
|
| 66 |
+
message={assistantMessage([
|
| 67 |
+
{ type: "reasoning", text: "I should search first" },
|
| 68 |
+
{
|
| 69 |
+
type: "tool-retrieve_tutor_context",
|
| 70 |
+
toolCallId: "retrieve-1",
|
| 71 |
+
state: "input-available",
|
| 72 |
+
input: { query: "transformer attention" },
|
| 73 |
+
},
|
| 74 |
+
])}
|
| 75 |
+
/>,
|
| 76 |
+
);
|
| 77 |
+
|
| 78 |
+
expect(
|
| 79 |
+
screen.getByRole("button", {
|
| 80 |
+
name: /Running Hybrid search transformer attention/,
|
| 81 |
+
}),
|
| 82 |
+
).toBeTruthy();
|
| 83 |
+
|
| 84 |
+
rerender(
|
| 85 |
+
<ChatMessage
|
| 86 |
+
isStreaming
|
| 87 |
+
showAssistantActions={false}
|
| 88 |
+
message={assistantMessage([
|
| 89 |
+
{ type: "reasoning", text: "I should search first" },
|
| 90 |
+
{
|
| 91 |
+
type: "tool-retrieve_tutor_context",
|
| 92 |
+
toolCallId: "retrieve-1",
|
| 93 |
+
state: "output-available",
|
| 94 |
+
input: { query: "transformer attention" },
|
| 95 |
+
output: {
|
| 96 |
+
matches: [
|
| 97 |
+
{ docId: "attention", url: "https://example.com/attention" },
|
| 98 |
+
],
|
| 99 |
+
},
|
| 100 |
+
},
|
| 101 |
+
{ type: "reasoning", text: "The result answers the question" },
|
| 102 |
+
])}
|
| 103 |
+
/>,
|
| 104 |
+
);
|
| 105 |
+
|
| 106 |
+
expect(
|
| 107 |
+
screen.getByRole("button", {
|
| 108 |
+
name: "Activity · 1 tool · 2 thoughts · 1 source",
|
| 109 |
+
}),
|
| 110 |
+
).toBeTruthy();
|
| 111 |
+
expect(screen.queryByText("Running")).toBeNull();
|
| 112 |
+
expect(screen.getByText("The result answers the question")).toBeTruthy();
|
| 113 |
+
});
|
| 114 |
+
|
| 115 |
+
it("uses the web icon for provider-native search tools", () => {
|
| 116 |
+
render(
|
| 117 |
+
<ChatMessage
|
| 118 |
+
showAssistantActions={false}
|
| 119 |
+
message={assistantMessage([
|
| 120 |
+
{
|
| 121 |
+
type: "tool-google_search",
|
| 122 |
+
toolCallId: "search-1",
|
| 123 |
+
state: "output-available",
|
| 124 |
+
input: { query: "latest LangChain release" },
|
| 125 |
+
output: { text: "Search results" },
|
| 126 |
+
},
|
| 127 |
+
])}
|
| 128 |
+
/>,
|
| 129 |
+
);
|
| 130 |
+
|
| 131 |
+
fireEvent.click(screen.getByRole("button", { name: "Activity · 1 tool" }));
|
| 132 |
+
const toolItem = screen.getByRole("listitem");
|
| 133 |
+
expect(toolItem.querySelector(".lucide-globe")).not.toBeNull();
|
| 134 |
+
expect(toolItem.querySelector(".lucide-database")).toBeNull();
|
| 135 |
+
});
|
| 136 |
+
|
| 137 |
+
it("bounds expanded tool output by both line and character limits", () => {
|
| 138 |
+
const lineLimitedOutput = "one\ntwo\nthree\nfour\nfive\nsix";
|
| 139 |
+
const characterLimitedOutput = `${"x".repeat(1005)}TAIL`;
|
| 140 |
+
|
| 141 |
+
render(
|
| 142 |
+
<ChatMessage
|
| 143 |
+
showAssistantActions={false}
|
| 144 |
+
message={assistantMessage([
|
| 145 |
+
{
|
| 146 |
+
type: "tool-run_kb_command",
|
| 147 |
+
toolCallId: "kb-1",
|
| 148 |
+
state: "output-available",
|
| 149 |
+
input: { text: "head raw/docs/file.md" },
|
| 150 |
+
output: { text: lineLimitedOutput },
|
| 151 |
+
},
|
| 152 |
+
{
|
| 153 |
+
type: "tool-custom_reader",
|
| 154 |
+
toolCallId: "custom-1",
|
| 155 |
+
state: "output-available",
|
| 156 |
+
input: { text: "read the long result" },
|
| 157 |
+
output: { text: characterLimitedOutput },
|
| 158 |
+
},
|
| 159 |
+
])}
|
| 160 |
+
/>,
|
| 161 |
+
);
|
| 162 |
+
|
| 163 |
+
fireEvent.click(
|
| 164 |
+
screen.getByRole("button", { name: "Activity · 2 tools" }),
|
| 165 |
+
);
|
| 166 |
+
fireEvent.click(
|
| 167 |
+
screen.getByRole("button", { name: /KB shell.*6 lines/ }),
|
| 168 |
+
);
|
| 169 |
+
|
| 170 |
+
const lineTruncation = screen.getByText(/truncated \(4 chars more\)/);
|
| 171 |
+
const linePreview = lineTruncation.closest("pre");
|
| 172 |
+
expect(linePreview?.firstChild?.textContent).toBe(
|
| 173 |
+
"one\ntwo\nthree\nfour\nfive",
|
| 174 |
+
);
|
| 175 |
+
expect(linePreview?.textContent).not.toContain("six");
|
| 176 |
+
|
| 177 |
+
fireEvent.click(
|
| 178 |
+
screen.getByRole("button", { name: /Custom Reader.*1\.0k chars/ }),
|
| 179 |
+
);
|
| 180 |
+
|
| 181 |
+
const characterTruncation = screen.getByText(/truncated \(9 chars more\)/);
|
| 182 |
+
const characterPreview = characterTruncation.closest("pre");
|
| 183 |
+
expect(characterPreview?.firstChild?.textContent).toHaveLength(1000);
|
| 184 |
+
expect(characterPreview?.textContent).not.toContain("TAIL");
|
| 185 |
+
});
|
| 186 |
+
});
|
| 187 |
+
|
| 188 |
+
describe("ChatMessage citations", () => {
|
| 189 |
+
it("reuses an inline citation number on its source card and keeps unsafe links inert", () => {
|
| 190 |
+
const { container } = render(
|
| 191 |
+
<ChatMessage
|
| 192 |
+
showAssistantActions={false}
|
| 193 |
+
message={assistantMessage([
|
| 194 |
+
{
|
| 195 |
+
type: "text",
|
| 196 |
+
text: [
|
| 197 |
+
"[Trusted](https://example.com/docs#section)",
|
| 198 |
+
"[Again](https://example.com/docs/)",
|
| 199 |
+
"[Unsafe](javascript:alert(1))",
|
| 200 |
+
"[Missing KB file](raw/docs/missing.md)",
|
| 201 |
+
].join(" "),
|
| 202 |
+
},
|
| 203 |
+
{
|
| 204 |
+
type: "data-source",
|
| 205 |
+
data: {
|
| 206 |
+
docId: "docs-1",
|
| 207 |
+
title: "Docs title",
|
| 208 |
+
url: "https://example.com/docs",
|
| 209 |
+
sourceKey: "example",
|
| 210 |
+
sourceLabel: "Example Docs",
|
| 211 |
+
score: 0.9,
|
| 212 |
+
group: "docs",
|
| 213 |
+
},
|
| 214 |
+
},
|
| 215 |
+
])}
|
| 216 |
+
/>,
|
| 217 |
+
);
|
| 218 |
+
|
| 219 |
+
const inlineChips = Array.from(
|
| 220 |
+
container.querySelectorAll<HTMLAnchorElement>("a.citation-chip"),
|
| 221 |
+
);
|
| 222 |
+
expect(inlineChips).toHaveLength(2);
|
| 223 |
+
expect(inlineChips.map((chip) => chip.textContent)).toEqual(["1", "1"]);
|
| 224 |
+
|
| 225 |
+
const sourceCard = screen.getByTitle("Docs · Example Docs");
|
| 226 |
+
expect(sourceCard.tagName).toBe("A");
|
| 227 |
+
expect(sourceCard.getAttribute("href")).toBe("https://example.com/docs");
|
| 228 |
+
expect(sourceCard.textContent).toContain("1");
|
| 229 |
+
expect(sourceCard.textContent).toContain("Docs title");
|
| 230 |
+
|
| 231 |
+
expect(screen.getByText("Unsafe").closest("a")).toBeNull();
|
| 232 |
+
expect(screen.getByText("Missing KB file").closest("a")).toBeNull();
|
| 233 |
+
});
|
| 234 |
+
});
|
frontend/components/chat-message.tsx
CHANGED
|
@@ -574,12 +574,12 @@ function ToolKindIcon({ type, className }: { type: string; className?: string })
|
|
| 574 |
if (name.includes("kb_command") || name.includes("shell") || name.includes("command")) {
|
| 575 |
return <Terminal className={className} />;
|
| 576 |
}
|
| 577 |
-
if (name.includes("retrieve") || name.includes("search")) {
|
| 578 |
-
return <Database className={className} />;
|
| 579 |
-
}
|
| 580 |
if (name.includes("web") || name.includes("url") || name.includes("google")) {
|
| 581 |
return <Globe className={className} />;
|
| 582 |
}
|
|
|
|
|
|
|
|
|
|
| 583 |
return <Wrench className={className} />;
|
| 584 |
}
|
| 585 |
|
|
|
|
| 574 |
if (name.includes("kb_command") || name.includes("shell") || name.includes("command")) {
|
| 575 |
return <Terminal className={className} />;
|
| 576 |
}
|
|
|
|
|
|
|
|
|
|
| 577 |
if (name.includes("web") || name.includes("url") || name.includes("google")) {
|
| 578 |
return <Globe className={className} />;
|
| 579 |
}
|
| 580 |
+
if (name.includes("retrieve") || name.includes("search")) {
|
| 581 |
+
return <Database className={className} />;
|
| 582 |
+
}
|
| 583 |
return <Wrench className={className} />;
|
| 584 |
}
|
| 585 |
|
frontend/components/chat-shell.test.tsx
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
| 2 |
+
import userEvent from "@testing-library/user-event";
|
| 3 |
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
| 4 |
+
|
| 5 |
+
const fetchToolsMock = vi.hoisted(() => vi.fn());
|
| 6 |
+
|
| 7 |
+
const chatHarness = vi.hoisted(() => ({
|
| 8 |
+
messages: [] as Array<{
|
| 9 |
+
id: string;
|
| 10 |
+
role: "assistant" | "user";
|
| 11 |
+
parts: Array<{ type: "text"; text: string }>;
|
| 12 |
+
}>,
|
| 13 |
+
status: "ready" as "error" | "ready" | "streaming" | "submitted",
|
| 14 |
+
error: null as Error | null,
|
| 15 |
+
onData: undefined as ((part: unknown) => void) | undefined,
|
| 16 |
+
sendMessage: vi.fn(),
|
| 17 |
+
setMessages: vi.fn(),
|
| 18 |
+
regenerate: vi.fn(),
|
| 19 |
+
stop: vi.fn(),
|
| 20 |
+
clearError: vi.fn(),
|
| 21 |
+
transportOptions: undefined as unknown,
|
| 22 |
+
}));
|
| 23 |
+
|
| 24 |
+
vi.mock("@/lib/api", async (importOriginal) => {
|
| 25 |
+
const original = await importOriginal<typeof import("@/lib/api")>();
|
| 26 |
+
return {
|
| 27 |
+
...original,
|
| 28 |
+
fetchTools: fetchToolsMock,
|
| 29 |
+
getApiBaseUrl: () => "http://localhost:8000",
|
| 30 |
+
};
|
| 31 |
+
});
|
| 32 |
+
|
| 33 |
+
vi.mock("@ai-sdk/react", async () => {
|
| 34 |
+
const { useReducer } = await import("react");
|
| 35 |
+
return {
|
| 36 |
+
useChat: (options: { onData?: (part: unknown) => void }) => {
|
| 37 |
+
const [, forceRender] = useReducer((version: number) => version + 1, 0);
|
| 38 |
+
chatHarness.onData = options.onData;
|
| 39 |
+
return {
|
| 40 |
+
messages: chatHarness.messages,
|
| 41 |
+
sendMessage: chatHarness.sendMessage,
|
| 42 |
+
setMessages: (next: typeof chatHarness.messages) => {
|
| 43 |
+
chatHarness.setMessages(next);
|
| 44 |
+
forceRender();
|
| 45 |
+
},
|
| 46 |
+
regenerate: chatHarness.regenerate,
|
| 47 |
+
stop: chatHarness.stop,
|
| 48 |
+
status: chatHarness.status,
|
| 49 |
+
error: chatHarness.error,
|
| 50 |
+
clearError: chatHarness.clearError,
|
| 51 |
+
};
|
| 52 |
+
},
|
| 53 |
+
};
|
| 54 |
+
});
|
| 55 |
+
|
| 56 |
+
vi.mock("ai", () => ({
|
| 57 |
+
DefaultChatTransport: class DefaultChatTransport {
|
| 58 |
+
constructor(options: unknown) {
|
| 59 |
+
chatHarness.transportOptions = options;
|
| 60 |
+
}
|
| 61 |
+
},
|
| 62 |
+
}));
|
| 63 |
+
|
| 64 |
+
vi.mock("@/components/source-sidebar", () => ({
|
| 65 |
+
SourceSidebar: ({
|
| 66 |
+
onNewChat,
|
| 67 |
+
onToggleSource,
|
| 68 |
+
onToggleTool,
|
| 69 |
+
sourceError,
|
| 70 |
+
}: {
|
| 71 |
+
onNewChat: () => void;
|
| 72 |
+
onToggleSource: (key: string) => void;
|
| 73 |
+
onToggleTool: (key: string) => void;
|
| 74 |
+
sourceError: string | null;
|
| 75 |
+
}) => (
|
| 76 |
+
<aside>
|
| 77 |
+
<button type="button" onClick={onNewChat}>
|
| 78 |
+
New chat
|
| 79 |
+
</button>
|
| 80 |
+
<button type="button" onClick={() => onToggleSource("course-default")}>
|
| 81 |
+
Toggle default course
|
| 82 |
+
</button>
|
| 83 |
+
<button type="button" onClick={() => onToggleSource("docs-optional")}>
|
| 84 |
+
Toggle optional docs
|
| 85 |
+
</button>
|
| 86 |
+
<button type="button" onClick={() => onToggleTool("web_search")}>
|
| 87 |
+
Toggle web search
|
| 88 |
+
</button>
|
| 89 |
+
{sourceError ? <p role="alert">{sourceError}</p> : null}
|
| 90 |
+
</aside>
|
| 91 |
+
),
|
| 92 |
+
}));
|
| 93 |
+
|
| 94 |
+
vi.mock("@/components/chat-message", () => ({
|
| 95 |
+
ChatMessage: ({
|
| 96 |
+
message,
|
| 97 |
+
isEditing,
|
| 98 |
+
editDraft,
|
| 99 |
+
onAssistantRedo,
|
| 100 |
+
onEditChange,
|
| 101 |
+
onEditSave,
|
| 102 |
+
onUserEdit,
|
| 103 |
+
}: {
|
| 104 |
+
message: {
|
| 105 |
+
id: string;
|
| 106 |
+
role: "assistant" | "user";
|
| 107 |
+
parts: Array<{ type: string; text?: string }>;
|
| 108 |
+
};
|
| 109 |
+
isEditing: boolean;
|
| 110 |
+
editDraft: string;
|
| 111 |
+
onAssistantRedo: (messageId: string) => void;
|
| 112 |
+
onEditChange: (value: string) => void;
|
| 113 |
+
onEditSave: (messageId: string) => void;
|
| 114 |
+
onUserEdit: (messageId: string) => void;
|
| 115 |
+
}) => {
|
| 116 |
+
const text = message.parts
|
| 117 |
+
.filter((part) => part.type === "text")
|
| 118 |
+
.map((part) => part.text ?? "")
|
| 119 |
+
.join("");
|
| 120 |
+
|
| 121 |
+
return (
|
| 122 |
+
<div data-testid={`message-${message.id}`}>
|
| 123 |
+
{isEditing ? (
|
| 124 |
+
<>
|
| 125 |
+
<textarea
|
| 126 |
+
aria-label={`Edit ${message.id}`}
|
| 127 |
+
value={editDraft}
|
| 128 |
+
onChange={(event) => onEditChange(event.target.value)}
|
| 129 |
+
/>
|
| 130 |
+
<button type="button" onClick={() => onEditSave(message.id)}>
|
| 131 |
+
Save {message.id}
|
| 132 |
+
</button>
|
| 133 |
+
</>
|
| 134 |
+
) : (
|
| 135 |
+
<span>{text}</span>
|
| 136 |
+
)}
|
| 137 |
+
{message.role === "user" && !isEditing ? (
|
| 138 |
+
<button type="button" onClick={() => onUserEdit(message.id)}>
|
| 139 |
+
Edit {message.id}
|
| 140 |
+
</button>
|
| 141 |
+
) : null}
|
| 142 |
+
{message.role === "assistant" ? (
|
| 143 |
+
<button type="button" onClick={() => onAssistantRedo(message.id)}>
|
| 144 |
+
Redo {message.id}
|
| 145 |
+
</button>
|
| 146 |
+
) : null}
|
| 147 |
+
</div>
|
| 148 |
+
);
|
| 149 |
+
},
|
| 150 |
+
}));
|
| 151 |
+
|
| 152 |
+
import { ChatShell } from "@/components/chat-shell";
|
| 153 |
+
|
| 154 |
+
const DEFAULT_MODEL = "deepseek:deepseek-v4-flash";
|
| 155 |
+
const CLAUDE_MODEL = "anthropic:claude-haiku-4-5";
|
| 156 |
+
|
| 157 |
+
const toolsResponse = {
|
| 158 |
+
model: DEFAULT_MODEL,
|
| 159 |
+
availableModels: [
|
| 160 |
+
{ id: DEFAULT_MODEL, label: "DeepSeek V4 Flash" },
|
| 161 |
+
{ id: CLAUDE_MODEL, label: "Claude Haiku 4.5" },
|
| 162 |
+
],
|
| 163 |
+
tools: [
|
| 164 |
+
{
|
| 165 |
+
kind: "configurable" as const,
|
| 166 |
+
key: "retrieval" as const,
|
| 167 |
+
label: "Knowledge base",
|
| 168 |
+
active: true,
|
| 169 |
+
sources: [
|
| 170 |
+
{
|
| 171 |
+
key: "course-default",
|
| 172 |
+
label: "Default course",
|
| 173 |
+
shortLabel: "Course",
|
| 174 |
+
group: "courses" as const,
|
| 175 |
+
selectedByDefault: true,
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
key: "docs-optional",
|
| 179 |
+
label: "Optional docs",
|
| 180 |
+
shortLabel: "Docs",
|
| 181 |
+
group: "docs" as const,
|
| 182 |
+
selectedByDefault: false,
|
| 183 |
+
},
|
| 184 |
+
],
|
| 185 |
+
},
|
| 186 |
+
{
|
| 187 |
+
kind: "toggle" as const,
|
| 188 |
+
key: "web_search",
|
| 189 |
+
label: "Web search",
|
| 190 |
+
active: true,
|
| 191 |
+
},
|
| 192 |
+
{
|
| 193 |
+
kind: "toggle" as const,
|
| 194 |
+
key: "url_context",
|
| 195 |
+
label: "URL context",
|
| 196 |
+
active: false,
|
| 197 |
+
},
|
| 198 |
+
],
|
| 199 |
+
};
|
| 200 |
+
|
| 201 |
+
function message(
|
| 202 |
+
id: string,
|
| 203 |
+
role: "assistant" | "user",
|
| 204 |
+
text: string,
|
| 205 |
+
) {
|
| 206 |
+
return { id, role, parts: [{ type: "text" as const, text }] };
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
function deferred<T>() {
|
| 210 |
+
let resolve!: (value: T) => void;
|
| 211 |
+
let reject!: (reason?: unknown) => void;
|
| 212 |
+
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
|
| 213 |
+
resolve = resolvePromise;
|
| 214 |
+
reject = rejectPromise;
|
| 215 |
+
});
|
| 216 |
+
return { promise, reject, resolve };
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
function sentBody(callIndex = 0) {
|
| 220 |
+
return chatHarness.sendMessage.mock.calls[callIndex]?.[1]?.body;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
beforeEach(() => {
|
| 224 |
+
chatHarness.messages = [];
|
| 225 |
+
chatHarness.status = "ready";
|
| 226 |
+
chatHarness.error = null;
|
| 227 |
+
chatHarness.onData = undefined;
|
| 228 |
+
chatHarness.transportOptions = undefined;
|
| 229 |
+
chatHarness.sendMessage.mockReset();
|
| 230 |
+
chatHarness.regenerate.mockReset();
|
| 231 |
+
chatHarness.stop.mockReset();
|
| 232 |
+
chatHarness.clearError.mockReset();
|
| 233 |
+
chatHarness.setMessages.mockReset();
|
| 234 |
+
chatHarness.setMessages.mockImplementation(
|
| 235 |
+
(next: typeof chatHarness.messages) => {
|
| 236 |
+
chatHarness.messages = next;
|
| 237 |
+
},
|
| 238 |
+
);
|
| 239 |
+
fetchToolsMock.mockReset();
|
| 240 |
+
fetchToolsMock.mockResolvedValue(toolsResponse);
|
| 241 |
+
});
|
| 242 |
+
|
| 243 |
+
describe("ChatShell request lifecycle", () => {
|
| 244 |
+
it("keeps Send disabled until the initial tool and model defaults are ready", async () => {
|
| 245 |
+
const pendingTools = deferred<typeof toolsResponse>();
|
| 246 |
+
fetchToolsMock.mockReturnValueOnce(pendingTools.promise);
|
| 247 |
+
const user = userEvent.setup();
|
| 248 |
+
|
| 249 |
+
render(<ChatShell />);
|
| 250 |
+
await user.type(screen.getByRole("textbox"), "What is RAG?");
|
| 251 |
+
|
| 252 |
+
expect(
|
| 253 |
+
(screen.getByRole("button", { name: "Send message" }) as HTMLButtonElement)
|
| 254 |
+
.disabled,
|
| 255 |
+
).toBe(true);
|
| 256 |
+
await user.keyboard("{Enter}");
|
| 257 |
+
const suggestion = screen
|
| 258 |
+
.getByRole("heading", { name: "Ask your AI tutor" })
|
| 259 |
+
.parentElement?.querySelector("button");
|
| 260 |
+
expect(suggestion).not.toBeNull();
|
| 261 |
+
await user.click(suggestion!);
|
| 262 |
+
expect(chatHarness.sendMessage).not.toHaveBeenCalled();
|
| 263 |
+
|
| 264 |
+
await act(async () => pendingTools.resolve(toolsResponse));
|
| 265 |
+
|
| 266 |
+
await waitFor(() =>
|
| 267 |
+
expect(
|
| 268 |
+
(screen.getByRole("button", {
|
| 269 |
+
name: "Send message",
|
| 270 |
+
}) as HTMLButtonElement).disabled,
|
| 271 |
+
).toBe(false),
|
| 272 |
+
);
|
| 273 |
+
});
|
| 274 |
+
|
| 275 |
+
it("submits the loaded model, default sources, and active tools", async () => {
|
| 276 |
+
const user = userEvent.setup();
|
| 277 |
+
render(<ChatShell />);
|
| 278 |
+
await screen.findByRole("button", { name: "DeepSeek V4 Flash" });
|
| 279 |
+
|
| 280 |
+
await user.type(screen.getByRole("textbox"), " Explain agents ");
|
| 281 |
+
await user.click(screen.getByRole("button", { name: "Send message" }));
|
| 282 |
+
|
| 283 |
+
expect(chatHarness.sendMessage).toHaveBeenCalledWith(
|
| 284 |
+
{ text: "Explain agents" },
|
| 285 |
+
{
|
| 286 |
+
body: {
|
| 287 |
+
sourceKeys: ["course-default"],
|
| 288 |
+
enabledTools: ["web_search"],
|
| 289 |
+
includeReasoning: true,
|
| 290 |
+
threadId: "",
|
| 291 |
+
model: DEFAULT_MODEL,
|
| 292 |
+
},
|
| 293 |
+
},
|
| 294 |
+
);
|
| 295 |
+
});
|
| 296 |
+
|
| 297 |
+
it("sends the user's current source and tool selections, including no sources", async () => {
|
| 298 |
+
const user = userEvent.setup();
|
| 299 |
+
render(<ChatShell />);
|
| 300 |
+
await screen.findByRole("button", { name: "DeepSeek V4 Flash" });
|
| 301 |
+
|
| 302 |
+
await user.click(screen.getByRole("button", { name: "Toggle optional docs" }));
|
| 303 |
+
await user.click(screen.getByRole("button", { name: "Toggle web search" }));
|
| 304 |
+
await user.type(screen.getByRole("textbox"), "Selected sources");
|
| 305 |
+
await user.click(screen.getByRole("button", { name: "Send message" }));
|
| 306 |
+
|
| 307 |
+
expect(sentBody()).toMatchObject({
|
| 308 |
+
sourceKeys: ["course-default", "docs-optional"],
|
| 309 |
+
enabledTools: [],
|
| 310 |
+
});
|
| 311 |
+
|
| 312 |
+
await user.click(screen.getByRole("button", { name: "Toggle default course" }));
|
| 313 |
+
await user.click(screen.getByRole("button", { name: "Toggle optional docs" }));
|
| 314 |
+
await user.type(screen.getByRole("textbox"), "No knowledge base");
|
| 315 |
+
await user.click(screen.getByRole("button", { name: "Send message" }));
|
| 316 |
+
|
| 317 |
+
expect(sentBody(1)).toMatchObject({ sourceKeys: [], enabledTools: [] });
|
| 318 |
+
});
|
| 319 |
+
|
| 320 |
+
it("adopts data-thread immediately and sends it with the next turn", async () => {
|
| 321 |
+
const user = userEvent.setup();
|
| 322 |
+
render(<ChatShell />);
|
| 323 |
+
await screen.findByRole("button", { name: "DeepSeek V4 Flash" });
|
| 324 |
+
|
| 325 |
+
await user.type(screen.getByRole("textbox"), "First question");
|
| 326 |
+
await user.click(screen.getByRole("button", { name: "Send message" }));
|
| 327 |
+
act(() => {
|
| 328 |
+
chatHarness.onData?.({
|
| 329 |
+
type: "data-thread",
|
| 330 |
+
data: { threadId: "thread-from-server" },
|
| 331 |
+
});
|
| 332 |
+
});
|
| 333 |
+
await user.type(screen.getByRole("textbox"), "Follow up");
|
| 334 |
+
await user.click(screen.getByRole("button", { name: "Send message" }));
|
| 335 |
+
|
| 336 |
+
expect(sentBody(1)).toMatchObject({ threadId: "thread-from-server" });
|
| 337 |
+
|
| 338 |
+
act(() => {
|
| 339 |
+
chatHarness.onData?.({
|
| 340 |
+
type: "data-thread",
|
| 341 |
+
data: { threadId: "replacement-thread" },
|
| 342 |
+
});
|
| 343 |
+
});
|
| 344 |
+
await user.type(screen.getByRole("textbox"), "One more turn");
|
| 345 |
+
await user.click(screen.getByRole("button", { name: "Send message" }));
|
| 346 |
+
|
| 347 |
+
expect(sentBody(2)).toMatchObject({ threadId: "replacement-thread" });
|
| 348 |
+
});
|
| 349 |
+
|
| 350 |
+
it("locks model switching for an existing conversation and unlocks on New chat", async () => {
|
| 351 |
+
chatHarness.messages = [message("u1", "user", "First question")];
|
| 352 |
+
const user = userEvent.setup();
|
| 353 |
+
render(<ChatShell />);
|
| 354 |
+
|
| 355 |
+
const modelPicker = await screen.findByRole("button", {
|
| 356 |
+
name: "DeepSeek V4 Flash",
|
| 357 |
+
});
|
| 358 |
+
expect((modelPicker as HTMLButtonElement).disabled).toBe(true);
|
| 359 |
+
|
| 360 |
+
await user.click(screen.getByRole("button", { name: "New chat" }));
|
| 361 |
+
|
| 362 |
+
expect(chatHarness.setMessages).toHaveBeenCalledWith([]);
|
| 363 |
+
await waitFor(() =>
|
| 364 |
+
expect((modelPicker as HTMLButtonElement).disabled).toBe(false),
|
| 365 |
+
);
|
| 366 |
+
});
|
| 367 |
+
|
| 368 |
+
it("clears a failed model-specific tools request after a successful retry", async () => {
|
| 369 |
+
fetchToolsMock
|
| 370 |
+
.mockResolvedValueOnce(toolsResponse)
|
| 371 |
+
.mockRejectedValueOnce(new Error("Unable to load Claude tools"))
|
| 372 |
+
.mockResolvedValueOnce(toolsResponse);
|
| 373 |
+
const user = userEvent.setup();
|
| 374 |
+
render(<ChatShell />);
|
| 375 |
+
|
| 376 |
+
await user.click(
|
| 377 |
+
await screen.findByRole("button", { name: "DeepSeek V4 Flash" }),
|
| 378 |
+
);
|
| 379 |
+
await user.click(screen.getByRole("option", { name: "Claude Haiku 4.5" }));
|
| 380 |
+
expect(await screen.findByRole("alert")).toHaveTextContent(
|
| 381 |
+
"Unable to load Claude tools",
|
| 382 |
+
);
|
| 383 |
+
|
| 384 |
+
await user.click(screen.getByRole("button", { name: "Claude Haiku 4.5" }));
|
| 385 |
+
await user.click(screen.getByRole("option", { name: "DeepSeek V4 Flash" }));
|
| 386 |
+
|
| 387 |
+
await waitFor(() => expect(fetchToolsMock).toHaveBeenCalledTimes(3));
|
| 388 |
+
await waitFor(() => expect(screen.queryByRole("alert")).toBeNull());
|
| 389 |
+
});
|
| 390 |
+
|
| 391 |
+
it("pauses submissions while model-specific tools are reloading", async () => {
|
| 392 |
+
const pendingClaudeTools = deferred<typeof toolsResponse>();
|
| 393 |
+
fetchToolsMock
|
| 394 |
+
.mockResolvedValueOnce(toolsResponse)
|
| 395 |
+
.mockReturnValueOnce(pendingClaudeTools.promise);
|
| 396 |
+
const user = userEvent.setup();
|
| 397 |
+
render(<ChatShell />);
|
| 398 |
+
|
| 399 |
+
await user.click(
|
| 400 |
+
await screen.findByRole("button", { name: "DeepSeek V4 Flash" }),
|
| 401 |
+
);
|
| 402 |
+
await user.click(screen.getByRole("option", { name: "Claude Haiku 4.5" }));
|
| 403 |
+
await waitFor(() => expect(fetchToolsMock).toHaveBeenCalledTimes(2));
|
| 404 |
+
|
| 405 |
+
await user.type(screen.getByRole("textbox"), "Compare the providers");
|
| 406 |
+
await waitFor(() =>
|
| 407 |
+
expect(
|
| 408 |
+
(screen.getByRole("button", {
|
| 409 |
+
name: "Send message",
|
| 410 |
+
}) as HTMLButtonElement).disabled,
|
| 411 |
+
).toBe(true),
|
| 412 |
+
);
|
| 413 |
+
await user.keyboard("{Enter}");
|
| 414 |
+
expect(chatHarness.sendMessage).not.toHaveBeenCalled();
|
| 415 |
+
|
| 416 |
+
await act(async () => pendingClaudeTools.resolve(toolsResponse));
|
| 417 |
+
await waitFor(() =>
|
| 418 |
+
expect(
|
| 419 |
+
(screen.getByRole("button", {
|
| 420 |
+
name: "Send message",
|
| 421 |
+
}) as HTMLButtonElement).disabled,
|
| 422 |
+
).toBe(false),
|
| 423 |
+
);
|
| 424 |
+
await user.click(screen.getByRole("button", { name: "Send message" }));
|
| 425 |
+
|
| 426 |
+
expect(sentBody()).toMatchObject({ model: CLAUDE_MODEL });
|
| 427 |
+
});
|
| 428 |
+
|
| 429 |
+
it("keeps the registry ready when the active model is selected again", async () => {
|
| 430 |
+
const user = userEvent.setup();
|
| 431 |
+
render(<ChatShell />);
|
| 432 |
+
|
| 433 |
+
await user.click(
|
| 434 |
+
await screen.findByRole("button", { name: "DeepSeek V4 Flash" }),
|
| 435 |
+
);
|
| 436 |
+
await user.click(screen.getByRole("option", { name: "DeepSeek V4 Flash" }));
|
| 437 |
+
expect(fetchToolsMock).toHaveBeenCalledOnce();
|
| 438 |
+
|
| 439 |
+
await user.type(screen.getByRole("textbox"), "Still ready");
|
| 440 |
+
const sendButton = screen.getByRole("button", { name: "Send message" });
|
| 441 |
+
expect((sendButton as HTMLButtonElement).disabled).toBe(false);
|
| 442 |
+
await user.click(sendButton);
|
| 443 |
+
expect(chatHarness.sendMessage).toHaveBeenCalledOnce();
|
| 444 |
+
});
|
| 445 |
+
|
| 446 |
+
it("clears a previous stream error and allows a retry", async () => {
|
| 447 |
+
chatHarness.status = "error";
|
| 448 |
+
chatHarness.error = new Error("The previous request failed");
|
| 449 |
+
const user = userEvent.setup();
|
| 450 |
+
render(<ChatShell />);
|
| 451 |
+
await screen.findByRole("button", { name: "DeepSeek V4 Flash" });
|
| 452 |
+
|
| 453 |
+
expect(screen.getByText("The previous request failed")).toBeVisible();
|
| 454 |
+
await user.type(screen.getByRole("textbox"), "Try again");
|
| 455 |
+
await user.click(screen.getByRole("button", { name: "Send message" }));
|
| 456 |
+
|
| 457 |
+
expect(chatHarness.clearError).toHaveBeenCalled();
|
| 458 |
+
expect(chatHarness.sendMessage).toHaveBeenCalledWith(
|
| 459 |
+
{ text: "Try again" },
|
| 460 |
+
expect.objectContaining({ body: expect.any(Object) }),
|
| 461 |
+
);
|
| 462 |
+
});
|
| 463 |
+
|
| 464 |
+
it.each(["submitted", "streaming"] as const)(
|
| 465 |
+
"offers Stop and aborts while the request is %s",
|
| 466 |
+
async (status) => {
|
| 467 |
+
chatHarness.status = status;
|
| 468 |
+
const user = userEvent.setup();
|
| 469 |
+
render(<ChatShell />);
|
| 470 |
+
|
| 471 |
+
await user.click(screen.getByRole("button", { name: "Stop generating" }));
|
| 472 |
+
|
| 473 |
+
expect(chatHarness.stop).toHaveBeenCalledTimes(1);
|
| 474 |
+
expect(chatHarness.sendMessage).not.toHaveBeenCalled();
|
| 475 |
+
},
|
| 476 |
+
);
|
| 477 |
+
|
| 478 |
+
it("passes the selected message boundary to edit and regenerate", async () => {
|
| 479 |
+
chatHarness.messages = [
|
| 480 |
+
message("u1", "user", "Original question"),
|
| 481 |
+
message("a1", "assistant", "First answer"),
|
| 482 |
+
message("u2", "user", "Later question"),
|
| 483 |
+
message("a2", "assistant", "Later answer"),
|
| 484 |
+
];
|
| 485 |
+
const user = userEvent.setup();
|
| 486 |
+
render(<ChatShell />);
|
| 487 |
+
await screen.findByRole("button", { name: "DeepSeek V4 Flash" });
|
| 488 |
+
|
| 489 |
+
await user.click(screen.getByRole("button", { name: "Redo a1" }));
|
| 490 |
+
expect(chatHarness.regenerate).toHaveBeenCalledWith({
|
| 491 |
+
messageId: "a1",
|
| 492 |
+
body: expect.objectContaining({
|
| 493 |
+
model: DEFAULT_MODEL,
|
| 494 |
+
threadId: "",
|
| 495 |
+
}),
|
| 496 |
+
});
|
| 497 |
+
|
| 498 |
+
await user.click(screen.getByRole("button", { name: "Edit u1" }));
|
| 499 |
+
const editor = screen.getByRole("textbox", { name: "Edit u1" });
|
| 500 |
+
fireEvent.change(editor, { target: { value: "Rewritten question" } });
|
| 501 |
+
await user.click(screen.getByRole("button", { name: "Save u1" }));
|
| 502 |
+
|
| 503 |
+
expect(chatHarness.sendMessage).toHaveBeenCalledWith(
|
| 504 |
+
{ text: "Rewritten question", messageId: "u1" },
|
| 505 |
+
{
|
| 506 |
+
body: expect.objectContaining({
|
| 507 |
+
model: DEFAULT_MODEL,
|
| 508 |
+
threadId: "",
|
| 509 |
+
}),
|
| 510 |
+
},
|
| 511 |
+
);
|
| 512 |
+
});
|
| 513 |
+
|
| 514 |
+
it("clears an adopted thread when starting a new chat", async () => {
|
| 515 |
+
const user = userEvent.setup();
|
| 516 |
+
render(<ChatShell />);
|
| 517 |
+
await screen.findByRole("button", { name: "DeepSeek V4 Flash" });
|
| 518 |
+
|
| 519 |
+
await user.type(screen.getByRole("textbox"), "First question");
|
| 520 |
+
await user.click(screen.getByRole("button", { name: "Send message" }));
|
| 521 |
+
act(() => {
|
| 522 |
+
chatHarness.onData?.({
|
| 523 |
+
type: "data-thread",
|
| 524 |
+
data: { threadId: "old-thread" },
|
| 525 |
+
});
|
| 526 |
+
});
|
| 527 |
+
await user.click(screen.getByRole("button", { name: "New chat" }));
|
| 528 |
+
await user.type(screen.getByRole("textbox"), "Fresh question");
|
| 529 |
+
await user.click(screen.getByRole("button", { name: "Send message" }));
|
| 530 |
+
|
| 531 |
+
expect(sentBody(1)).toMatchObject({ threadId: "" });
|
| 532 |
+
});
|
| 533 |
+
|
| 534 |
+
it("ignores a late thread event from a stream abandoned by New chat", async () => {
|
| 535 |
+
chatHarness.status = "streaming";
|
| 536 |
+
const user = userEvent.setup();
|
| 537 |
+
render(<ChatShell />);
|
| 538 |
+
await waitFor(() => expect(fetchToolsMock).toHaveBeenCalledOnce());
|
| 539 |
+
|
| 540 |
+
await user.click(screen.getByRole("button", { name: "New chat" }));
|
| 541 |
+
expect(chatHarness.stop).toHaveBeenCalledOnce();
|
| 542 |
+
|
| 543 |
+
act(() => {
|
| 544 |
+
chatHarness.onData?.({
|
| 545 |
+
type: "data-thread",
|
| 546 |
+
data: { threadId: "late-abandoned-thread" },
|
| 547 |
+
});
|
| 548 |
+
});
|
| 549 |
+
|
| 550 |
+
chatHarness.status = "ready";
|
| 551 |
+
await user.type(screen.getByRole("textbox"), "Fresh after abort");
|
| 552 |
+
await user.click(screen.getByRole("button", { name: "Send message" }));
|
| 553 |
+
|
| 554 |
+
expect(sentBody()).toMatchObject({ threadId: "" });
|
| 555 |
+
});
|
| 556 |
+
});
|
frontend/components/chat-shell.tsx
CHANGED
|
@@ -75,6 +75,7 @@ export function ChatShell() {
|
|
| 75 |
const [selectedSourceKeys, setSelectedSourceKeys] = useState<string[]>([]);
|
| 76 |
const [enabledToolKeys, setEnabledToolKeys] = useState<string[]>([]);
|
| 77 |
const [sourceError, setSourceError] = useState<string | null>(null);
|
|
|
|
| 78 |
const [input, setInput] = useState("");
|
| 79 |
const [threadId, setThreadId] = useState("");
|
| 80 |
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
|
|
@@ -84,6 +85,7 @@ export function ChatShell() {
|
|
| 84 |
const composerInputRef = useRef<HTMLTextAreaElement>(null);
|
| 85 |
const pendingScrollRef = useRef(false);
|
| 86 |
const spacerRef = useRef<HTMLDivElement>(null);
|
|
|
|
| 87 |
|
| 88 |
const {
|
| 89 |
messages,
|
|
@@ -97,7 +99,7 @@ export function ChatShell() {
|
|
| 97 |
} = useChat({
|
| 98 |
transport,
|
| 99 |
onData: (part) => {
|
| 100 |
-
if (part.type === "data-thread") {
|
| 101 |
const nextThreadId = (part as ThreadDataPart).data.threadId;
|
| 102 |
if (nextThreadId) {
|
| 103 |
// Urgent update on purpose: deferring it (startTransition) lets a
|
|
@@ -141,6 +143,7 @@ export function ChatShell() {
|
|
| 141 |
.map((tool) => tool.key),
|
| 142 |
);
|
| 143 |
setSourceError(null);
|
|
|
|
| 144 |
} catch (loadError) {
|
| 145 |
if (controller.signal.aborted) {
|
| 146 |
return;
|
|
@@ -150,6 +153,7 @@ export function ChatShell() {
|
|
| 150 |
? loadError.message
|
| 151 |
: "Unable to load tool registry.",
|
| 152 |
);
|
|
|
|
| 153 |
}
|
| 154 |
}
|
| 155 |
|
|
@@ -179,6 +183,8 @@ export function ChatShell() {
|
|
| 179 |
.filter((tool) => tool.kind === "toggle" && tool.active)
|
| 180 |
.map((tool) => tool.key),
|
| 181 |
);
|
|
|
|
|
|
|
| 182 |
} catch (loadError) {
|
| 183 |
if (controller.signal.aborted) {
|
| 184 |
return;
|
|
@@ -188,6 +194,7 @@ export function ChatShell() {
|
|
| 188 |
? loadError.message
|
| 189 |
: "Unable to load tool registry.",
|
| 190 |
);
|
|
|
|
| 191 |
}
|
| 192 |
}
|
| 193 |
|
|
@@ -199,7 +206,8 @@ export function ChatShell() {
|
|
| 199 |
// After a stream error the SDK parks status on "error" until clearError()
|
| 200 |
// or the next request; handleSubmit clears it, so the error state must
|
| 201 |
// stay sendable or the Send button locks out mouse users for good.
|
| 202 |
-
const canSend =
|
|
|
|
| 203 |
const typedMessages = messages as TutorMessage[];
|
| 204 |
const latestMessage = typedMessages[typedMessages.length - 1];
|
| 205 |
const streamingAssistantId =
|
|
@@ -282,11 +290,12 @@ export function ChatShell() {
|
|
| 282 |
|
| 283 |
async function handleSubmit(override?: string) {
|
| 284 |
const trimmed = (override ?? input).trim();
|
| 285 |
-
if (!trimmed ||
|
| 286 |
return;
|
| 287 |
}
|
| 288 |
|
| 289 |
clearError();
|
|
|
|
| 290 |
setCopiedMessageId(null);
|
| 291 |
setEditingMessageId(null);
|
| 292 |
setEditingText("");
|
|
@@ -307,7 +316,11 @@ export function ChatShell() {
|
|
| 307 |
}
|
| 308 |
|
| 309 |
async function handleRedo(messageId?: string) {
|
|
|
|
|
|
|
|
|
|
| 310 |
clearError();
|
|
|
|
| 311 |
setCopiedMessageId(null);
|
| 312 |
await regenerate({
|
| 313 |
messageId,
|
|
@@ -362,11 +375,12 @@ export function ChatShell() {
|
|
| 362 |
|
| 363 |
async function handleEditSave(messageId: string) {
|
| 364 |
const trimmed = editingText.trim();
|
| 365 |
-
if (!trimmed ||
|
| 366 |
return;
|
| 367 |
}
|
| 368 |
|
| 369 |
clearError();
|
|
|
|
| 370 |
setCopiedMessageId(null);
|
| 371 |
setEditingMessageId(null);
|
| 372 |
setEditingText("");
|
|
@@ -386,6 +400,7 @@ export function ChatShell() {
|
|
| 386 |
}
|
| 387 |
|
| 388 |
function handleNewChat() {
|
|
|
|
| 389 |
if (isStreaming) {
|
| 390 |
stop();
|
| 391 |
}
|
|
@@ -500,7 +515,13 @@ export function ChatShell() {
|
|
| 500 |
<ModelPicker
|
| 501 |
availableModels={availableModels}
|
| 502 |
locked={typedMessages.length > 0}
|
| 503 |
-
onSelect={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
selectedModel={selectedModel}
|
| 505 |
/>
|
| 506 |
<ComposerActionButton
|
|
|
|
| 75 |
const [selectedSourceKeys, setSelectedSourceKeys] = useState<string[]>([]);
|
| 76 |
const [enabledToolKeys, setEnabledToolKeys] = useState<string[]>([]);
|
| 77 |
const [sourceError, setSourceError] = useState<string | null>(null);
|
| 78 |
+
const [toolRegistryReady, setToolRegistryReady] = useState(false);
|
| 79 |
const [input, setInput] = useState("");
|
| 80 |
const [threadId, setThreadId] = useState("");
|
| 81 |
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
|
|
|
|
| 85 |
const composerInputRef = useRef<HTMLTextAreaElement>(null);
|
| 86 |
const pendingScrollRef = useRef(false);
|
| 87 |
const spacerRef = useRef<HTMLDivElement>(null);
|
| 88 |
+
const acceptThreadDataRef = useRef(false);
|
| 89 |
|
| 90 |
const {
|
| 91 |
messages,
|
|
|
|
| 99 |
} = useChat({
|
| 100 |
transport,
|
| 101 |
onData: (part) => {
|
| 102 |
+
if (part.type === "data-thread" && acceptThreadDataRef.current) {
|
| 103 |
const nextThreadId = (part as ThreadDataPart).data.threadId;
|
| 104 |
if (nextThreadId) {
|
| 105 |
// Urgent update on purpose: deferring it (startTransition) lets a
|
|
|
|
| 143 |
.map((tool) => tool.key),
|
| 144 |
);
|
| 145 |
setSourceError(null);
|
| 146 |
+
setToolRegistryReady(true);
|
| 147 |
} catch (loadError) {
|
| 148 |
if (controller.signal.aborted) {
|
| 149 |
return;
|
|
|
|
| 153 |
? loadError.message
|
| 154 |
: "Unable to load tool registry.",
|
| 155 |
);
|
| 156 |
+
setToolRegistryReady(false);
|
| 157 |
}
|
| 158 |
}
|
| 159 |
|
|
|
|
| 183 |
.filter((tool) => tool.kind === "toggle" && tool.active)
|
| 184 |
.map((tool) => tool.key),
|
| 185 |
);
|
| 186 |
+
setSourceError(null);
|
| 187 |
+
setToolRegistryReady(true);
|
| 188 |
} catch (loadError) {
|
| 189 |
if (controller.signal.aborted) {
|
| 190 |
return;
|
|
|
|
| 194 |
? loadError.message
|
| 195 |
: "Unable to load tool registry.",
|
| 196 |
);
|
| 197 |
+
setToolRegistryReady(false);
|
| 198 |
}
|
| 199 |
}
|
| 200 |
|
|
|
|
| 206 |
// After a stream error the SDK parks status on "error" until clearError()
|
| 207 |
// or the next request; handleSubmit clears it, so the error state must
|
| 208 |
// stay sendable or the Send button locks out mouse users for good.
|
| 209 |
+
const canSend =
|
| 210 |
+
toolRegistryReady && (status === "ready" || status === "error");
|
| 211 |
const typedMessages = messages as TutorMessage[];
|
| 212 |
const latestMessage = typedMessages[typedMessages.length - 1];
|
| 213 |
const streamingAssistantId =
|
|
|
|
| 290 |
|
| 291 |
async function handleSubmit(override?: string) {
|
| 292 |
const trimmed = (override ?? input).trim();
|
| 293 |
+
if (!trimmed || !canSend) {
|
| 294 |
return;
|
| 295 |
}
|
| 296 |
|
| 297 |
clearError();
|
| 298 |
+
acceptThreadDataRef.current = true;
|
| 299 |
setCopiedMessageId(null);
|
| 300 |
setEditingMessageId(null);
|
| 301 |
setEditingText("");
|
|
|
|
| 316 |
}
|
| 317 |
|
| 318 |
async function handleRedo(messageId?: string) {
|
| 319 |
+
if (!canSend) {
|
| 320 |
+
return;
|
| 321 |
+
}
|
| 322 |
clearError();
|
| 323 |
+
acceptThreadDataRef.current = true;
|
| 324 |
setCopiedMessageId(null);
|
| 325 |
await regenerate({
|
| 326 |
messageId,
|
|
|
|
| 375 |
|
| 376 |
async function handleEditSave(messageId: string) {
|
| 377 |
const trimmed = editingText.trim();
|
| 378 |
+
if (!trimmed || !canSend) {
|
| 379 |
return;
|
| 380 |
}
|
| 381 |
|
| 382 |
clearError();
|
| 383 |
+
acceptThreadDataRef.current = true;
|
| 384 |
setCopiedMessageId(null);
|
| 385 |
setEditingMessageId(null);
|
| 386 |
setEditingText("");
|
|
|
|
| 400 |
}
|
| 401 |
|
| 402 |
function handleNewChat() {
|
| 403 |
+
acceptThreadDataRef.current = false;
|
| 404 |
if (isStreaming) {
|
| 405 |
stop();
|
| 406 |
}
|
|
|
|
| 515 |
<ModelPicker
|
| 516 |
availableModels={availableModels}
|
| 517 |
locked={typedMessages.length > 0}
|
| 518 |
+
onSelect={(modelId) => {
|
| 519 |
+
if (modelId === selectedModel) {
|
| 520 |
+
return;
|
| 521 |
+
}
|
| 522 |
+
setToolRegistryReady(false);
|
| 523 |
+
setSelectedModel(modelId);
|
| 524 |
+
}}
|
| 525 |
selectedModel={selectedModel}
|
| 526 |
/>
|
| 527 |
<ComposerActionButton
|
frontend/components/markdown-block.test.tsx
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { cleanup, render, screen } from "@testing-library/react";
|
| 2 |
+
import { afterEach, describe, expect, it } from "vitest";
|
| 3 |
+
|
| 4 |
+
import { MarkdownBlock } from "@/components/markdown-block";
|
| 5 |
+
|
| 6 |
+
afterEach(cleanup);
|
| 7 |
+
|
| 8 |
+
describe("MarkdownBlock links", () => {
|
| 9 |
+
it("renders short numbered links as chips but retains long claim text", () => {
|
| 10 |
+
const url = "https://example.com/reference";
|
| 11 |
+
const longClaim =
|
| 12 |
+
"This linked sentence is a substantive claim that should stay readable in the answer";
|
| 13 |
+
|
| 14 |
+
render(
|
| 15 |
+
<MarkdownBlock citationNumbers={new Map([[url, 1]])}>
|
| 16 |
+
{`[Short](${url}) and [${longClaim}](${url})`}
|
| 17 |
+
</MarkdownBlock>,
|
| 18 |
+
);
|
| 19 |
+
|
| 20 |
+
const chips = screen.getAllByRole("link", { name: "1" });
|
| 21 |
+
expect(chips).toHaveLength(2);
|
| 22 |
+
expect(chips.every((chip) => chip.getAttribute("href") === url)).toBe(true);
|
| 23 |
+
expect(screen.queryByText("Short")).toBeNull();
|
| 24 |
+
expect(screen.getByText(longClaim)).toBeTruthy();
|
| 25 |
+
});
|
| 26 |
+
|
| 27 |
+
it("allows external HTTP and email links while leaving unsafe and relative links inert", () => {
|
| 28 |
+
render(
|
| 29 |
+
<MarkdownBlock>
|
| 30 |
+
{
|
| 31 |
+
"[Website](https://safe.example/path) [Email](mailto:tutor@example.com) [Unsafe](javascript:alert(1)) [Local](raw/docs/private.md)"
|
| 32 |
+
}
|
| 33 |
+
</MarkdownBlock>,
|
| 34 |
+
);
|
| 35 |
+
|
| 36 |
+
const website = screen.getByRole("link", { name: "Website" });
|
| 37 |
+
expect(website.getAttribute("href")).toBe("https://safe.example/path");
|
| 38 |
+
expect(website.getAttribute("target")).toBe("_blank");
|
| 39 |
+
expect(website.getAttribute("rel")).toBe("noreferrer");
|
| 40 |
+
|
| 41 |
+
const email = screen.getByRole("link", { name: "Email" });
|
| 42 |
+
expect(email.getAttribute("href")).toBe("mailto:tutor@example.com");
|
| 43 |
+
expect(email.getAttribute("target")).toBe("_blank");
|
| 44 |
+
expect(email.getAttribute("rel")).toBe("noreferrer");
|
| 45 |
+
|
| 46 |
+
expect(screen.getByText("Unsafe").closest("a")).toBeNull();
|
| 47 |
+
expect(screen.getByText("Local").closest("a")).toBeNull();
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
it("resolves KB references before rendering and gives equivalent references the same number", () => {
|
| 51 |
+
const resolvedUrl = "https://docs.example.com/guide";
|
| 52 |
+
const citationNumbers = new Map([[resolvedUrl, 7]]);
|
| 53 |
+
const citationResolutions = new Map([
|
| 54 |
+
["raw/docs/guide.md", resolvedUrl],
|
| 55 |
+
["kb://doc/guide-id", resolvedUrl],
|
| 56 |
+
]);
|
| 57 |
+
|
| 58 |
+
render(
|
| 59 |
+
<MarkdownBlock
|
| 60 |
+
citationNumbers={citationNumbers}
|
| 61 |
+
citationResolutions={citationResolutions}
|
| 62 |
+
>
|
| 63 |
+
{"[Raw path](raw/docs/guide.md) [Document ID](kb://doc/guide-id)"}
|
| 64 |
+
</MarkdownBlock>,
|
| 65 |
+
);
|
| 66 |
+
|
| 67 |
+
const chips = screen.getAllByRole("link", { name: "7" });
|
| 68 |
+
expect(chips).toHaveLength(2);
|
| 69 |
+
expect(chips.map((chip) => chip.getAttribute("href"))).toEqual([
|
| 70 |
+
resolvedUrl,
|
| 71 |
+
resolvedUrl,
|
| 72 |
+
]);
|
| 73 |
+
expect(chips.map((chip) => chip.getAttribute("title"))).toEqual([
|
| 74 |
+
"Raw path",
|
| 75 |
+
"Document ID",
|
| 76 |
+
]);
|
| 77 |
+
});
|
| 78 |
+
});
|
frontend/components/source-sidebar.test.tsx
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
| 2 |
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
| 3 |
+
|
| 4 |
+
import { SourceSidebar } from "@/components/source-sidebar";
|
| 5 |
+
import type { TutorSource, TutorTool } from "@/lib/api";
|
| 6 |
+
|
| 7 |
+
afterEach(cleanup);
|
| 8 |
+
|
| 9 |
+
const sources: TutorSource[] = [
|
| 10 |
+
{
|
| 11 |
+
key: "course-a",
|
| 12 |
+
label: "Course A",
|
| 13 |
+
shortLabel: "Course A",
|
| 14 |
+
description: "Learn practical agent engineering.",
|
| 15 |
+
infoUrl: "https://academy.example.com/course-a",
|
| 16 |
+
group: "courses",
|
| 17 |
+
selectedByDefault: true,
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
key: "docs-a",
|
| 21 |
+
label: "Documentation A",
|
| 22 |
+
shortLabel: "Docs A",
|
| 23 |
+
description: "Reference documentation for the library.",
|
| 24 |
+
infoUrl: "https://docs.example.com/",
|
| 25 |
+
group: "docs",
|
| 26 |
+
selectedByDefault: false,
|
| 27 |
+
version: "v1.2",
|
| 28 |
+
indexedAt: "2026-07-01",
|
| 29 |
+
},
|
| 30 |
+
];
|
| 31 |
+
|
| 32 |
+
const tools: TutorTool[] = [
|
| 33 |
+
{
|
| 34 |
+
kind: "configurable",
|
| 35 |
+
key: "retrieval",
|
| 36 |
+
label: "Knowledge base",
|
| 37 |
+
active: true,
|
| 38 |
+
sources,
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
kind: "toggle",
|
| 42 |
+
key: "web_search",
|
| 43 |
+
label: "Web search",
|
| 44 |
+
active: false,
|
| 45 |
+
},
|
| 46 |
+
];
|
| 47 |
+
|
| 48 |
+
function renderSidebar() {
|
| 49 |
+
const handlers = {
|
| 50 |
+
onNewChat: vi.fn(),
|
| 51 |
+
onToggleSource: vi.fn(),
|
| 52 |
+
onToggleTool: vi.fn(),
|
| 53 |
+
};
|
| 54 |
+
render(
|
| 55 |
+
<SourceSidebar
|
| 56 |
+
{...handlers}
|
| 57 |
+
selectedSourceKeys={["course-a"]}
|
| 58 |
+
enabledToolKeys={["web_search"]}
|
| 59 |
+
sourceError={null}
|
| 60 |
+
tools={tools}
|
| 61 |
+
/>,
|
| 62 |
+
);
|
| 63 |
+
return handlers;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
describe("SourceSidebar", () => {
|
| 67 |
+
it("reports active tools and routes new-chat, source, and tool actions", () => {
|
| 68 |
+
const handlers = renderSidebar();
|
| 69 |
+
|
| 70 |
+
expect(screen.getByText("2/2")).toBeTruthy();
|
| 71 |
+
expect(
|
| 72 |
+
screen.getByRole("button", { name: "Course A" }).getAttribute(
|
| 73 |
+
"aria-pressed",
|
| 74 |
+
),
|
| 75 |
+
).toBe("true");
|
| 76 |
+
expect(
|
| 77 |
+
screen.getByRole("button", { name: "Docs A" }).getAttribute(
|
| 78 |
+
"aria-pressed",
|
| 79 |
+
),
|
| 80 |
+
).toBe("false");
|
| 81 |
+
expect(
|
| 82 |
+
screen.getByRole("button", { name: "Web search" }).getAttribute(
|
| 83 |
+
"aria-pressed",
|
| 84 |
+
),
|
| 85 |
+
).toBe("true");
|
| 86 |
+
|
| 87 |
+
fireEvent.click(screen.getByRole("button", { name: "New chat" }));
|
| 88 |
+
fireEvent.click(screen.getByRole("button", { name: "Docs A" }));
|
| 89 |
+
fireEvent.click(screen.getByRole("button", { name: "Web search" }));
|
| 90 |
+
|
| 91 |
+
expect(handlers.onNewChat).toHaveBeenCalledOnce();
|
| 92 |
+
expect(handlers.onToggleSource).toHaveBeenCalledWith("docs-a");
|
| 93 |
+
expect(handlers.onToggleTool).toHaveBeenCalledWith("web_search");
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
it("shows registry metadata in a portaled source popover and closes it with Escape", () => {
|
| 97 |
+
renderSidebar();
|
| 98 |
+
|
| 99 |
+
const infoButton = screen.getByRole("button", {
|
| 100 |
+
name: "About Documentation A",
|
| 101 |
+
});
|
| 102 |
+
expect(infoButton.getAttribute("aria-expanded")).toBe("false");
|
| 103 |
+
|
| 104 |
+
fireEvent.click(infoButton);
|
| 105 |
+
|
| 106 |
+
const dialog = screen.getByRole("dialog");
|
| 107 |
+
expect(dialog.textContent).toContain(
|
| 108 |
+
"Reference documentation for the library.",
|
| 109 |
+
);
|
| 110 |
+
expect(dialog.textContent).toContain("v1.2 · indexed Jul 2026");
|
| 111 |
+
const docsLink = screen.getByRole("link", { name: /View docs/ });
|
| 112 |
+
expect(docsLink.getAttribute("href")).toBe("https://docs.example.com/");
|
| 113 |
+
expect(docsLink.getAttribute("target")).toBe("_blank");
|
| 114 |
+
|
| 115 |
+
fireEvent.keyDown(document, { key: "Escape" });
|
| 116 |
+
expect(screen.queryByRole("dialog")).toBeNull();
|
| 117 |
+
expect(infoButton.getAttribute("aria-expanded")).toBe("false");
|
| 118 |
+
});
|
| 119 |
+
});
|
frontend/components/source-sidebar.tsx
CHANGED
|
@@ -480,7 +480,11 @@ function formatIndexedMonth(isoDate: string | null | undefined): string | null {
|
|
| 480 |
if (!isoDate) return null;
|
| 481 |
const date = new Date(isoDate);
|
| 482 |
if (Number.isNaN(date.getTime())) return null;
|
| 483 |
-
return date.toLocaleString("en-US", {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 484 |
}
|
| 485 |
|
| 486 |
function getPopoverInfo(source: TutorSource): PopoverInfo | undefined {
|
|
|
|
| 480 |
if (!isoDate) return null;
|
| 481 |
const date = new Date(isoDate);
|
| 482 |
if (Number.isNaN(date.getTime())) return null;
|
| 483 |
+
return date.toLocaleString("en-US", {
|
| 484 |
+
month: "short",
|
| 485 |
+
year: "numeric",
|
| 486 |
+
timeZone: "UTC",
|
| 487 |
+
});
|
| 488 |
}
|
| 489 |
|
| 490 |
function getPopoverInfo(source: TutorSource): PopoverInfo | undefined {
|
frontend/e2e/chat-flow.spec.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { expect, test, type Page } from "@playwright/test";
|
| 2 |
+
|
| 3 |
+
const MODEL = "deepseek:deepseek-v4-flash";
|
| 4 |
+
const largeToolOutput = "retrieved context ".repeat(20_000);
|
| 5 |
+
|
| 6 |
+
const toolsResponse = {
|
| 7 |
+
model: MODEL,
|
| 8 |
+
availableModels: [{ id: MODEL, label: "DeepSeek V4 Flash" }],
|
| 9 |
+
tools: [
|
| 10 |
+
{
|
| 11 |
+
kind: "configurable",
|
| 12 |
+
key: "retrieval",
|
| 13 |
+
label: "Knowledge base",
|
| 14 |
+
active: true,
|
| 15 |
+
sources: [
|
| 16 |
+
{
|
| 17 |
+
key: "example-docs",
|
| 18 |
+
label: "Example documentation",
|
| 19 |
+
shortLabel: "Example docs",
|
| 20 |
+
description: "Documentation used by the browser test.",
|
| 21 |
+
infoUrl: "https://example.test/docs",
|
| 22 |
+
group: "docs",
|
| 23 |
+
selectedByDefault: true,
|
| 24 |
+
version: "v1.0",
|
| 25 |
+
indexedAt: "2026-07-01",
|
| 26 |
+
},
|
| 27 |
+
],
|
| 28 |
+
},
|
| 29 |
+
],
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
function sse(parts: Array<Record<string, unknown>>) {
|
| 33 |
+
return `${parts.map((part) => `data: ${JSON.stringify(part)}\n\n`).join("")}data: [DONE]\n\n`;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
async function mockTools(page: Page) {
|
| 37 |
+
await page.route("**/api/tools**", (route) =>
|
| 38 |
+
route.fulfill({
|
| 39 |
+
status: 200,
|
| 40 |
+
contentType: "application/json",
|
| 41 |
+
body: JSON.stringify(toolsResponse),
|
| 42 |
+
}),
|
| 43 |
+
);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
test("streams activity and sends a compact follow-up on the same thread", async ({
|
| 47 |
+
page,
|
| 48 |
+
}) => {
|
| 49 |
+
const requests: Array<Record<string, unknown>> = [];
|
| 50 |
+
await mockTools(page);
|
| 51 |
+
await page.route("**/api/chat", async (route) => {
|
| 52 |
+
requests.push(route.request().postDataJSON() as Record<string, unknown>);
|
| 53 |
+
const firstTurn = requests.length === 1;
|
| 54 |
+
const parts = firstTurn
|
| 55 |
+
? [
|
| 56 |
+
{
|
| 57 |
+
type: "data-thread",
|
| 58 |
+
data: { threadId: "thread-browser" },
|
| 59 |
+
transient: true,
|
| 60 |
+
},
|
| 61 |
+
{ type: "start", messageId: "assistant-1" },
|
| 62 |
+
{ type: "start-step" },
|
| 63 |
+
{ type: "reasoning-start", id: "thought-1" },
|
| 64 |
+
{ type: "reasoning-delta", id: "thought-1", delta: "First thought" },
|
| 65 |
+
{ type: "reasoning-end", id: "thought-1" },
|
| 66 |
+
{
|
| 67 |
+
type: "tool-input-available",
|
| 68 |
+
toolCallId: "call-1",
|
| 69 |
+
toolName: "retrieve_tutor_context",
|
| 70 |
+
input: { query: "LoRA" },
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
type: "tool-output-available",
|
| 74 |
+
toolCallId: "call-1",
|
| 75 |
+
output: {
|
| 76 |
+
text: largeToolOutput,
|
| 77 |
+
matches: [
|
| 78 |
+
{
|
| 79 |
+
docId: "lora-doc",
|
| 80 |
+
title: "LoRA guide",
|
| 81 |
+
url: "https://example.test/lora",
|
| 82 |
+
sourceKey: "example-docs",
|
| 83 |
+
sourceLabel: "Example docs",
|
| 84 |
+
score: 0.9,
|
| 85 |
+
group: "docs",
|
| 86 |
+
},
|
| 87 |
+
],
|
| 88 |
+
},
|
| 89 |
+
},
|
| 90 |
+
{ type: "reasoning-start", id: "thought-2" },
|
| 91 |
+
{ type: "reasoning-delta", id: "thought-2", delta: "Second thought" },
|
| 92 |
+
{ type: "reasoning-end", id: "thought-2" },
|
| 93 |
+
{
|
| 94 |
+
type: "source-url",
|
| 95 |
+
sourceId: "lora-doc",
|
| 96 |
+
url: "https://example.test/lora",
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
type: "data-source",
|
| 100 |
+
data: {
|
| 101 |
+
docId: "lora-doc",
|
| 102 |
+
title: "LoRA guide",
|
| 103 |
+
url: "https://example.test/lora",
|
| 104 |
+
sourceKey: "example-docs",
|
| 105 |
+
sourceLabel: "Example docs",
|
| 106 |
+
score: 0.9,
|
| 107 |
+
group: "docs",
|
| 108 |
+
},
|
| 109 |
+
},
|
| 110 |
+
{ type: "text-start", id: "answer-1" },
|
| 111 |
+
{
|
| 112 |
+
type: "text-delta",
|
| 113 |
+
id: "answer-1",
|
| 114 |
+
delta: "LoRA is parameter-efficient [1](https://example.test/lora).",
|
| 115 |
+
},
|
| 116 |
+
{ type: "text-end", id: "answer-1" },
|
| 117 |
+
{ type: "finish-step" },
|
| 118 |
+
{ type: "finish" },
|
| 119 |
+
]
|
| 120 |
+
: [
|
| 121 |
+
{ type: "start", messageId: "assistant-2" },
|
| 122 |
+
{ type: "start-step" },
|
| 123 |
+
{ type: "text-start", id: "answer-2" },
|
| 124 |
+
{ type: "text-delta", id: "answer-2", delta: "Follow-up answer" },
|
| 125 |
+
{ type: "text-end", id: "answer-2" },
|
| 126 |
+
{ type: "finish-step" },
|
| 127 |
+
{ type: "finish" },
|
| 128 |
+
];
|
| 129 |
+
await route.fulfill({
|
| 130 |
+
status: 200,
|
| 131 |
+
headers: {
|
| 132 |
+
"content-type": "text/event-stream",
|
| 133 |
+
"x-vercel-ai-ui-message-stream": "v1",
|
| 134 |
+
},
|
| 135 |
+
body: sse(parts),
|
| 136 |
+
});
|
| 137 |
+
});
|
| 138 |
+
|
| 139 |
+
await page.goto("/");
|
| 140 |
+
await expect(page.getByRole("button", { name: "DeepSeek V4 Flash" })).toBeVisible();
|
| 141 |
+
const composer = page.getByRole("textbox");
|
| 142 |
+
await composer.fill("What is LoRA?");
|
| 143 |
+
await page.getByRole("button", { name: "Send message" }).click();
|
| 144 |
+
|
| 145 |
+
await expect(page.getByText("LoRA is parameter-efficient")).toBeVisible();
|
| 146 |
+
const activity = page.getByRole("button", {
|
| 147 |
+
name: "Activity · 1 tool · 2 thoughts · 1 source",
|
| 148 |
+
});
|
| 149 |
+
await expect(activity).toBeVisible();
|
| 150 |
+
await activity.click();
|
| 151 |
+
await expect(page.getByText("First thought")).toBeVisible();
|
| 152 |
+
await expect(page.getByText("Second thought")).toBeVisible();
|
| 153 |
+
|
| 154 |
+
await composer.fill("Can you expand?");
|
| 155 |
+
await page.getByRole("button", { name: "Send message" }).click();
|
| 156 |
+
await expect(page.getByText("Follow-up answer")).toBeVisible();
|
| 157 |
+
await expect.poll(() => requests.length).toBe(2);
|
| 158 |
+
|
| 159 |
+
const followUp = requests[1];
|
| 160 |
+
expect(followUp.threadId).toBe("thread-browser");
|
| 161 |
+
expect(JSON.stringify(followUp)).not.toContain(largeToolOutput.slice(0, 500));
|
| 162 |
+
expect(followUp.messages).toEqual([
|
| 163 |
+
expect.objectContaining({
|
| 164 |
+
role: "user",
|
| 165 |
+
parts: [{ type: "text", text: "What is LoRA?" }],
|
| 166 |
+
}),
|
| 167 |
+
expect.objectContaining({
|
| 168 |
+
role: "assistant",
|
| 169 |
+
parts: [
|
| 170 |
+
{
|
| 171 |
+
type: "text",
|
| 172 |
+
text: "LoRA is parameter-efficient [1](https://example.test/lora).",
|
| 173 |
+
},
|
| 174 |
+
],
|
| 175 |
+
}),
|
| 176 |
+
expect.objectContaining({
|
| 177 |
+
role: "user",
|
| 178 |
+
parts: [{ type: "text", text: "Can you expand?" }],
|
| 179 |
+
}),
|
| 180 |
+
]);
|
| 181 |
+
});
|
| 182 |
+
|
| 183 |
+
test("keeps controls and source metadata inside a narrow viewport", async ({ page }) => {
|
| 184 |
+
await page.setViewportSize({ width: 375, height: 812 });
|
| 185 |
+
await mockTools(page);
|
| 186 |
+
await page.goto("/");
|
| 187 |
+
|
| 188 |
+
await expect(page.getByRole("button", { name: "Send message" })).toBeVisible();
|
| 189 |
+
expect(
|
| 190 |
+
await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth),
|
| 191 |
+
).toBe(true);
|
| 192 |
+
|
| 193 |
+
await page
|
| 194 |
+
.getByRole("button", { name: "About Example documentation" })
|
| 195 |
+
.click();
|
| 196 |
+
const dialog = page.getByRole("dialog");
|
| 197 |
+
await expect(dialog).toContainText("v1.0 · indexed Jul 2026");
|
| 198 |
+
const box = await dialog.boundingBox();
|
| 199 |
+
expect(box).not.toBeNull();
|
| 200 |
+
expect(box!.x).toBeGreaterThanOrEqual(0);
|
| 201 |
+
expect(box!.x + box!.width).toBeLessThanOrEqual(375);
|
| 202 |
+
});
|
frontend/lib/api.test.mts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import assert from "node:assert/strict";
|
| 2 |
+
import test from "node:test";
|
| 3 |
+
import { fetchTools, getApiBaseUrl, type ToolsResponse } from "./api.ts";
|
| 4 |
+
|
| 5 |
+
const ENV_NAME = "NEXT_PUBLIC_AI_TUTOR_API_BASE_URL";
|
| 6 |
+
|
| 7 |
+
function restoreEnvironment(previous: string | undefined) {
|
| 8 |
+
if (previous === undefined) {
|
| 9 |
+
delete process.env[ENV_NAME];
|
| 10 |
+
} else {
|
| 11 |
+
process.env[ENV_NAME] = previous;
|
| 12 |
+
}
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
test("getApiBaseUrl prefers configured URLs and removes trailing slashes", () => {
|
| 16 |
+
const previous = process.env[ENV_NAME];
|
| 17 |
+
try {
|
| 18 |
+
process.env[ENV_NAME] = "https://api.example.test/tutor///";
|
| 19 |
+
assert.equal(getApiBaseUrl(), "https://api.example.test/tutor");
|
| 20 |
+
} finally {
|
| 21 |
+
restoreEnvironment(previous);
|
| 22 |
+
}
|
| 23 |
+
});
|
| 24 |
+
|
| 25 |
+
test("getApiBaseUrl uses the browser origin, then the server fallback", () => {
|
| 26 |
+
const previousEnv = process.env[ENV_NAME];
|
| 27 |
+
const previousWindow = Object.getOwnPropertyDescriptor(globalThis, "window");
|
| 28 |
+
try {
|
| 29 |
+
delete process.env[ENV_NAME];
|
| 30 |
+
Object.defineProperty(globalThis, "window", {
|
| 31 |
+
configurable: true,
|
| 32 |
+
value: { location: { origin: "https://browser.example.test" } },
|
| 33 |
+
});
|
| 34 |
+
assert.equal(getApiBaseUrl(), "https://browser.example.test");
|
| 35 |
+
|
| 36 |
+
delete (globalThis as { window?: unknown }).window;
|
| 37 |
+
assert.equal(getApiBaseUrl(), "http://127.0.0.1:8000");
|
| 38 |
+
} finally {
|
| 39 |
+
restoreEnvironment(previousEnv);
|
| 40 |
+
if (previousWindow) {
|
| 41 |
+
Object.defineProperty(globalThis, "window", previousWindow);
|
| 42 |
+
} else {
|
| 43 |
+
delete (globalThis as { window?: unknown }).window;
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
});
|
| 47 |
+
|
| 48 |
+
test("fetchTools sends the model query and AbortSignal and returns parsed JSON", async () => {
|
| 49 |
+
const previousEnv = process.env[ENV_NAME];
|
| 50 |
+
const previousFetch = globalThis.fetch;
|
| 51 |
+
const calls: Array<{ url: string; init: RequestInit | undefined }> = [];
|
| 52 |
+
const expected: ToolsResponse = {
|
| 53 |
+
model: "deepseek:deepseek-v4-flash",
|
| 54 |
+
availableModels: [
|
| 55 |
+
{ id: "deepseek:deepseek-v4-flash", label: "DeepSeek V4 Flash" },
|
| 56 |
+
],
|
| 57 |
+
tools: [],
|
| 58 |
+
};
|
| 59 |
+
const controller = new AbortController();
|
| 60 |
+
|
| 61 |
+
try {
|
| 62 |
+
process.env[ENV_NAME] = "https://api.example.test/base/";
|
| 63 |
+
globalThis.fetch = (async (input, init) => {
|
| 64 |
+
calls.push({ url: String(input), init });
|
| 65 |
+
return new Response(JSON.stringify(expected), {
|
| 66 |
+
status: 200,
|
| 67 |
+
headers: { "content-type": "application/json" },
|
| 68 |
+
});
|
| 69 |
+
}) as typeof fetch;
|
| 70 |
+
|
| 71 |
+
const actual = await fetchTools(
|
| 72 |
+
controller.signal,
|
| 73 |
+
"deepseek:deepseek-v4-flash",
|
| 74 |
+
);
|
| 75 |
+
|
| 76 |
+
assert.deepEqual(actual, expected);
|
| 77 |
+
assert.equal(calls.length, 1);
|
| 78 |
+
assert.equal(
|
| 79 |
+
calls[0]?.url,
|
| 80 |
+
"https://api.example.test/base/api/tools?model=deepseek%3Adeepseek-v4-flash",
|
| 81 |
+
);
|
| 82 |
+
assert.equal(calls[0]?.init?.method, "GET");
|
| 83 |
+
assert.equal(calls[0]?.init?.signal, controller.signal);
|
| 84 |
+
} finally {
|
| 85 |
+
restoreEnvironment(previousEnv);
|
| 86 |
+
globalThis.fetch = previousFetch;
|
| 87 |
+
}
|
| 88 |
+
});
|
| 89 |
+
|
| 90 |
+
test("fetchTools omits an empty model and reports non-success status codes", async () => {
|
| 91 |
+
const previousEnv = process.env[ENV_NAME];
|
| 92 |
+
const previousFetch = globalThis.fetch;
|
| 93 |
+
let requestedUrl = "";
|
| 94 |
+
|
| 95 |
+
try {
|
| 96 |
+
process.env[ENV_NAME] = "https://api.example.test";
|
| 97 |
+
globalThis.fetch = (async (input) => {
|
| 98 |
+
requestedUrl = String(input);
|
| 99 |
+
return new Response("unavailable", { status: 503 });
|
| 100 |
+
}) as typeof fetch;
|
| 101 |
+
|
| 102 |
+
await assert.rejects(fetchTools(undefined, ""), {
|
| 103 |
+
name: "Error",
|
| 104 |
+
message: "Unable to load tools (503)",
|
| 105 |
+
});
|
| 106 |
+
assert.equal(requestedUrl, "https://api.example.test/api/tools");
|
| 107 |
+
} finally {
|
| 108 |
+
restoreEnvironment(previousEnv);
|
| 109 |
+
globalThis.fetch = previousFetch;
|
| 110 |
+
}
|
| 111 |
+
});
|
frontend/lib/chat-http-transport.test.mts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import assert from "node:assert/strict";
|
| 2 |
+
import test from "node:test";
|
| 3 |
+
|
| 4 |
+
import { Chat } from "@ai-sdk/react";
|
| 5 |
+
import { DefaultChatTransport, type UIMessage, type UIMessageChunk } from "ai";
|
| 6 |
+
|
| 7 |
+
import { prepareTutorChatRequest } from "./chat-transport.ts";
|
| 8 |
+
|
| 9 |
+
function sseResponse(chunks: UIMessageChunk[]) {
|
| 10 |
+
const body = [
|
| 11 |
+
...chunks.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`),
|
| 12 |
+
"data: [DONE]\n\n",
|
| 13 |
+
].join("");
|
| 14 |
+
return new Response(body, {
|
| 15 |
+
status: 200,
|
| 16 |
+
headers: {
|
| 17 |
+
"content-type": "text/event-stream",
|
| 18 |
+
"x-vercel-ai-ui-message-stream": "v1",
|
| 19 |
+
},
|
| 20 |
+
});
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
test("DefaultChatTransport sends compact history and preserves checkpoint identity", async () => {
|
| 24 |
+
const largeToolOutput = "retrieved context ".repeat(20_000);
|
| 25 |
+
const originalAssistantText = " Exact streamed answer \n";
|
| 26 |
+
const initialMessages = [
|
| 27 |
+
{
|
| 28 |
+
id: "user-1",
|
| 29 |
+
role: "user",
|
| 30 |
+
parts: [{ type: "text", text: "Question" }],
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
id: "assistant-1",
|
| 34 |
+
role: "assistant",
|
| 35 |
+
parts: [
|
| 36 |
+
{ type: "reasoning", text: "Private thought" },
|
| 37 |
+
{
|
| 38 |
+
type: "tool-retrieve_tutor_context",
|
| 39 |
+
toolCallId: "call-1",
|
| 40 |
+
state: "output-available",
|
| 41 |
+
input: { query: "Question" },
|
| 42 |
+
output: { text: largeToolOutput, matches: [] },
|
| 43 |
+
},
|
| 44 |
+
{ type: "source-url", sourceId: "source-1", url: "https://example.test" },
|
| 45 |
+
{ type: "text", text: originalAssistantText },
|
| 46 |
+
],
|
| 47 |
+
},
|
| 48 |
+
] as unknown as UIMessage[];
|
| 49 |
+
|
| 50 |
+
let requestBody: Record<string, unknown> | undefined;
|
| 51 |
+
const receivedData: unknown[] = [];
|
| 52 |
+
const transport = new DefaultChatTransport<UIMessage>({
|
| 53 |
+
api: "https://api.example.test/api/chat",
|
| 54 |
+
prepareSendMessagesRequest: prepareTutorChatRequest,
|
| 55 |
+
fetch: async (_input, init) => {
|
| 56 |
+
requestBody = JSON.parse(String(init?.body)) as Record<string, unknown>;
|
| 57 |
+
return sseResponse([
|
| 58 |
+
{
|
| 59 |
+
type: "data-thread",
|
| 60 |
+
data: { threadId: "thread-1" },
|
| 61 |
+
transient: true,
|
| 62 |
+
},
|
| 63 |
+
{ type: "start", messageId: "assistant-2" },
|
| 64 |
+
{ type: "start-step" },
|
| 65 |
+
{ type: "text-start", id: "answer-2" },
|
| 66 |
+
{ type: "text-delta", id: "answer-2", delta: "Follow-up answer" },
|
| 67 |
+
{ type: "text-end", id: "answer-2" },
|
| 68 |
+
{ type: "finish-step" },
|
| 69 |
+
{ type: "finish" },
|
| 70 |
+
]);
|
| 71 |
+
},
|
| 72 |
+
});
|
| 73 |
+
const chat = new Chat<UIMessage>({
|
| 74 |
+
id: "chat-1",
|
| 75 |
+
messages: initialMessages,
|
| 76 |
+
transport,
|
| 77 |
+
onData: (part) => receivedData.push(part),
|
| 78 |
+
});
|
| 79 |
+
|
| 80 |
+
await chat.sendMessage(
|
| 81 |
+
{ text: "Follow up" },
|
| 82 |
+
{
|
| 83 |
+
body: {
|
| 84 |
+
threadId: "thread-1",
|
| 85 |
+
model: "deepseek:deepseek-v4-flash",
|
| 86 |
+
},
|
| 87 |
+
},
|
| 88 |
+
);
|
| 89 |
+
|
| 90 |
+
assert.ok(requestBody);
|
| 91 |
+
assert.equal(requestBody.threadId, "thread-1");
|
| 92 |
+
assert.equal(requestBody.model, "deepseek:deepseek-v4-flash");
|
| 93 |
+
assert.deepEqual(requestBody.messages, [
|
| 94 |
+
{
|
| 95 |
+
id: "user-1",
|
| 96 |
+
role: "user",
|
| 97 |
+
parts: [{ type: "text", text: "Question" }],
|
| 98 |
+
},
|
| 99 |
+
{
|
| 100 |
+
id: "assistant-1",
|
| 101 |
+
role: "assistant",
|
| 102 |
+
parts: [{ type: "text", text: originalAssistantText }],
|
| 103 |
+
},
|
| 104 |
+
{
|
| 105 |
+
id: chat.messages[2]?.id,
|
| 106 |
+
role: "user",
|
| 107 |
+
parts: [{ type: "text", text: "Follow up" }],
|
| 108 |
+
},
|
| 109 |
+
]);
|
| 110 |
+
assert.ok(JSON.stringify(requestBody).length < 2_000);
|
| 111 |
+
assert.deepEqual(receivedData, [
|
| 112 |
+
{
|
| 113 |
+
type: "data-thread",
|
| 114 |
+
data: { threadId: "thread-1" },
|
| 115 |
+
transient: true,
|
| 116 |
+
},
|
| 117 |
+
]);
|
| 118 |
+
|
| 119 |
+
const retainedTool = chat.messages[1]?.parts.find((part) =>
|
| 120 |
+
part.type.startsWith("tool-"),
|
| 121 |
+
);
|
| 122 |
+
assert.equal(
|
| 123 |
+
(retainedTool as { output: { text: string } }).output.text,
|
| 124 |
+
largeToolOutput,
|
| 125 |
+
);
|
| 126 |
+
assert.equal(
|
| 127 |
+
(chat.messages[1]?.parts.at(-1) as { text: string }).text,
|
| 128 |
+
originalAssistantText,
|
| 129 |
+
);
|
| 130 |
+
});
|
frontend/lib/chat-sdk-lifecycle.test.mts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import assert from "node:assert/strict";
|
| 2 |
+
import test from "node:test";
|
| 3 |
+
|
| 4 |
+
import { Chat } from "@ai-sdk/react";
|
| 5 |
+
import type { ChatTransport, UIMessage, UIMessageChunk } from "ai";
|
| 6 |
+
|
| 7 |
+
type SendOptions = Parameters<ChatTransport<UIMessage>["sendMessages"]>[0];
|
| 8 |
+
|
| 9 |
+
function assistantStream(messageId: string) {
|
| 10 |
+
const chunks: UIMessageChunk[] = [
|
| 11 |
+
{ type: "start", messageId },
|
| 12 |
+
{ type: "start-step" },
|
| 13 |
+
{ type: "text-start", id: `${messageId}-text` },
|
| 14 |
+
{ type: "text-delta", id: `${messageId}-text`, delta: "New answer" },
|
| 15 |
+
{ type: "text-end", id: `${messageId}-text` },
|
| 16 |
+
{ type: "finish-step" },
|
| 17 |
+
{ type: "finish" },
|
| 18 |
+
];
|
| 19 |
+
return new ReadableStream<UIMessageChunk>({
|
| 20 |
+
start(controller) {
|
| 21 |
+
for (const chunk of chunks) controller.enqueue(chunk);
|
| 22 |
+
controller.close();
|
| 23 |
+
},
|
| 24 |
+
});
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
class CapturingTransport implements ChatTransport<UIMessage> {
|
| 28 |
+
calls: Array<Omit<SendOptions, "messages"> & { messages: UIMessage[] }> = [];
|
| 29 |
+
|
| 30 |
+
async sendMessages(options: SendOptions) {
|
| 31 |
+
this.calls.push({
|
| 32 |
+
...options,
|
| 33 |
+
messages: structuredClone(options.messages),
|
| 34 |
+
});
|
| 35 |
+
return assistantStream(`response-${this.calls.length}`);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
async reconnectToStream() {
|
| 39 |
+
return null;
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
function transcript(): UIMessage[] {
|
| 44 |
+
return [
|
| 45 |
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "Question 1" }] },
|
| 46 |
+
{ id: "a1", role: "assistant", parts: [{ type: "text", text: "Answer 1" }] },
|
| 47 |
+
{ id: "u2", role: "user", parts: [{ type: "text", text: "Question 2" }] },
|
| 48 |
+
{ id: "a2", role: "assistant", parts: [{ type: "text", text: "Answer 2" }] },
|
| 49 |
+
{ id: "u3", role: "user", parts: [{ type: "text", text: "Question 3" }] },
|
| 50 |
+
{ id: "a3", role: "assistant", parts: [{ type: "text", text: "Answer 3" }] },
|
| 51 |
+
];
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
function messageSummary(messages: UIMessage[]) {
|
| 55 |
+
return messages.map((message) => ({
|
| 56 |
+
id: message.id,
|
| 57 |
+
role: message.role,
|
| 58 |
+
text: message.parts
|
| 59 |
+
.filter((part) => part.type === "text")
|
| 60 |
+
.map((part) => part.text)
|
| 61 |
+
.join(""),
|
| 62 |
+
}));
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
test("editing a user turn replaces it and drops its answer and later turns", async () => {
|
| 66 |
+
const transport = new CapturingTransport();
|
| 67 |
+
const chat = new Chat<UIMessage>({
|
| 68 |
+
id: "chat-edit",
|
| 69 |
+
messages: transcript(),
|
| 70 |
+
transport,
|
| 71 |
+
});
|
| 72 |
+
|
| 73 |
+
await chat.sendMessage({ text: "Question 2 edited", messageId: "u2" });
|
| 74 |
+
|
| 75 |
+
assert.equal(transport.calls.length, 1);
|
| 76 |
+
assert.equal(transport.calls[0].trigger, "submit-message");
|
| 77 |
+
assert.equal(transport.calls[0].messageId, "u2");
|
| 78 |
+
assert.deepEqual(messageSummary(transport.calls[0].messages), [
|
| 79 |
+
{ id: "u1", role: "user", text: "Question 1" },
|
| 80 |
+
{ id: "a1", role: "assistant", text: "Answer 1" },
|
| 81 |
+
{ id: "u2", role: "user", text: "Question 2 edited" },
|
| 82 |
+
]);
|
| 83 |
+
});
|
| 84 |
+
|
| 85 |
+
test("regenerating an assistant turn replays its user prompt and drops later turns", async () => {
|
| 86 |
+
const transport = new CapturingTransport();
|
| 87 |
+
const chat = new Chat<UIMessage>({
|
| 88 |
+
id: "chat-regenerate",
|
| 89 |
+
messages: transcript(),
|
| 90 |
+
transport,
|
| 91 |
+
});
|
| 92 |
+
|
| 93 |
+
await chat.regenerate({ messageId: "a2" });
|
| 94 |
+
|
| 95 |
+
assert.equal(transport.calls.length, 1);
|
| 96 |
+
assert.equal(transport.calls[0].trigger, "regenerate-message");
|
| 97 |
+
assert.equal(transport.calls[0].messageId, "a2");
|
| 98 |
+
assert.deepEqual(messageSummary(transport.calls[0].messages), [
|
| 99 |
+
{ id: "u1", role: "user", text: "Question 1" },
|
| 100 |
+
{ id: "a1", role: "assistant", text: "Answer 1" },
|
| 101 |
+
{ id: "u2", role: "user", text: "Question 2" },
|
| 102 |
+
]);
|
| 103 |
+
});
|
| 104 |
+
|
| 105 |
+
test("regenerating an earlier answer removes every subsequent turn", async () => {
|
| 106 |
+
const transport = new CapturingTransport();
|
| 107 |
+
const chat = new Chat<UIMessage>({
|
| 108 |
+
id: "chat-regenerate-earlier",
|
| 109 |
+
messages: transcript(),
|
| 110 |
+
transport,
|
| 111 |
+
});
|
| 112 |
+
|
| 113 |
+
await chat.regenerate({ messageId: "a1" });
|
| 114 |
+
|
| 115 |
+
assert.deepEqual(messageSummary(transport.calls[0].messages), [
|
| 116 |
+
{ id: "u1", role: "user", text: "Question 1" },
|
| 117 |
+
]);
|
| 118 |
+
});
|
frontend/lib/chat-stream.test.mts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import assert from "node:assert/strict";
|
| 2 |
+
import test from "node:test";
|
| 3 |
+
|
| 4 |
+
import {
|
| 5 |
+
readUIMessageStream,
|
| 6 |
+
type UIMessage,
|
| 7 |
+
type UIMessageChunk,
|
| 8 |
+
} from "ai";
|
| 9 |
+
|
| 10 |
+
import { compactChatMessages } from "./chat-transport.ts";
|
| 11 |
+
|
| 12 |
+
function streamChunks(chunks: UIMessageChunk[]) {
|
| 13 |
+
return new ReadableStream<UIMessageChunk>({
|
| 14 |
+
start(controller) {
|
| 15 |
+
for (const chunk of chunks) {
|
| 16 |
+
controller.enqueue(chunk);
|
| 17 |
+
}
|
| 18 |
+
controller.close();
|
| 19 |
+
},
|
| 20 |
+
});
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
async function foldChunks(chunks: UIMessageChunk[]) {
|
| 24 |
+
let latest: UIMessage | undefined;
|
| 25 |
+
for await (const message of readUIMessageStream({
|
| 26 |
+
stream: streamChunks(chunks),
|
| 27 |
+
})) {
|
| 28 |
+
latest = message;
|
| 29 |
+
}
|
| 30 |
+
assert.ok(latest, "the stream should produce an assistant message");
|
| 31 |
+
return latest;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
test("folds reasoning, tools, sources, and text in provider order", async () => {
|
| 35 |
+
const message = await foldChunks([
|
| 36 |
+
{ type: "start", messageId: "assistant-1" },
|
| 37 |
+
{ type: "start-step" },
|
| 38 |
+
{ type: "reasoning-start", id: "thought-1" },
|
| 39 |
+
{ type: "reasoning-delta", id: "thought-1", delta: "First thought" },
|
| 40 |
+
{ type: "reasoning-end", id: "thought-1" },
|
| 41 |
+
{
|
| 42 |
+
type: "tool-input-start",
|
| 43 |
+
toolCallId: "call-1",
|
| 44 |
+
toolName: "retrieve_tutor_context",
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
type: "tool-input-available",
|
| 48 |
+
toolCallId: "call-1",
|
| 49 |
+
toolName: "retrieve_tutor_context",
|
| 50 |
+
input: { query: "LoRA" },
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
type: "tool-output-available",
|
| 54 |
+
toolCallId: "call-1",
|
| 55 |
+
output: { text: "retrieved context", matches: [] },
|
| 56 |
+
},
|
| 57 |
+
{ type: "reasoning-start", id: "thought-2" },
|
| 58 |
+
{ type: "reasoning-delta", id: "thought-2", delta: "Second thought" },
|
| 59 |
+
{ type: "reasoning-end", id: "thought-2" },
|
| 60 |
+
{
|
| 61 |
+
type: "source-url",
|
| 62 |
+
sourceId: "source-1",
|
| 63 |
+
url: "https://example.com/lora",
|
| 64 |
+
title: "LoRA guide",
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
type: "data-source",
|
| 68 |
+
data: {
|
| 69 |
+
title: "LoRA guide",
|
| 70 |
+
url: "https://example.com/lora",
|
| 71 |
+
source: "docs",
|
| 72 |
+
},
|
| 73 |
+
},
|
| 74 |
+
{ type: "text-start", id: "answer-1" },
|
| 75 |
+
{
|
| 76 |
+
type: "text-delta",
|
| 77 |
+
id: "answer-1",
|
| 78 |
+
delta: " LoRA is parameter-efficient.\n",
|
| 79 |
+
},
|
| 80 |
+
{ type: "text-end", id: "answer-1" },
|
| 81 |
+
{ type: "finish-step" },
|
| 82 |
+
{ type: "finish" },
|
| 83 |
+
]);
|
| 84 |
+
|
| 85 |
+
assert.equal(message.id, "assistant-1");
|
| 86 |
+
assert.equal(message.role, "assistant");
|
| 87 |
+
assert.deepEqual(
|
| 88 |
+
message.parts.map((part) => part.type),
|
| 89 |
+
[
|
| 90 |
+
"step-start",
|
| 91 |
+
"reasoning",
|
| 92 |
+
"tool-retrieve_tutor_context",
|
| 93 |
+
"reasoning",
|
| 94 |
+
"source-url",
|
| 95 |
+
"data-source",
|
| 96 |
+
"text",
|
| 97 |
+
],
|
| 98 |
+
);
|
| 99 |
+
assert.equal(
|
| 100 |
+
(message.parts[1] as { text: string }).text,
|
| 101 |
+
"First thought",
|
| 102 |
+
);
|
| 103 |
+
assert.equal(
|
| 104 |
+
(message.parts[3] as { text: string }).text,
|
| 105 |
+
"Second thought",
|
| 106 |
+
);
|
| 107 |
+
assert.equal(
|
| 108 |
+
(message.parts[6] as { text: string }).text,
|
| 109 |
+
" LoRA is parameter-efficient.\n",
|
| 110 |
+
);
|
| 111 |
+
});
|
| 112 |
+
|
| 113 |
+
test("folded tool state stays in the UI while follow-up history is text-only", async () => {
|
| 114 |
+
const largeOutput = "retrieved context ".repeat(20_000);
|
| 115 |
+
const assistant = await foldChunks([
|
| 116 |
+
{ type: "start", messageId: "assistant-1" },
|
| 117 |
+
{ type: "start-step" },
|
| 118 |
+
{
|
| 119 |
+
type: "tool-input-available",
|
| 120 |
+
toolCallId: "call-1",
|
| 121 |
+
toolName: "retrieve_tutor_context",
|
| 122 |
+
input: { query: "LoRA" },
|
| 123 |
+
},
|
| 124 |
+
{
|
| 125 |
+
type: "tool-output-available",
|
| 126 |
+
toolCallId: "call-1",
|
| 127 |
+
output: { text: largeOutput, matches: [] },
|
| 128 |
+
},
|
| 129 |
+
{ type: "text-start", id: "answer-1" },
|
| 130 |
+
{ type: "text-delta", id: "answer-1", delta: "Exact answer \n" },
|
| 131 |
+
{ type: "text-end", id: "answer-1" },
|
| 132 |
+
{ type: "finish-step" },
|
| 133 |
+
{ type: "finish" },
|
| 134 |
+
]);
|
| 135 |
+
|
| 136 |
+
const toolPart = assistant.parts.find((part) => part.type.startsWith("tool-"));
|
| 137 |
+
assert.ok(toolPart);
|
| 138 |
+
assert.equal(
|
| 139 |
+
(toolPart as { output: { text: string } }).output.text,
|
| 140 |
+
largeOutput,
|
| 141 |
+
);
|
| 142 |
+
|
| 143 |
+
const compacted = compactChatMessages([
|
| 144 |
+
{ id: "user-1", role: "user", parts: [{ type: "text", text: "Question" }] },
|
| 145 |
+
assistant,
|
| 146 |
+
{
|
| 147 |
+
id: "user-2",
|
| 148 |
+
role: "user",
|
| 149 |
+
parts: [{ type: "text", text: "Follow up" }],
|
| 150 |
+
},
|
| 151 |
+
]);
|
| 152 |
+
|
| 153 |
+
assert.deepEqual(compacted[1].parts, [
|
| 154 |
+
{ type: "text", text: "Exact answer \n" },
|
| 155 |
+
]);
|
| 156 |
+
assert.ok(JSON.stringify(compacted).length < 1_000);
|
| 157 |
+
assert.equal(
|
| 158 |
+
(toolPart as { output: { text: string } }).output.text,
|
| 159 |
+
largeOutput,
|
| 160 |
+
"request compaction must not mutate folded UI state",
|
| 161 |
+
);
|
| 162 |
+
});
|
frontend/lib/chat-ui.test.mts
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import assert from "node:assert/strict";
|
| 2 |
+
import test from "node:test";
|
| 3 |
+
import type { SourcePartData } from "./api.ts";
|
| 4 |
+
import {
|
| 5 |
+
buildActivityItems,
|
| 6 |
+
buildCitationNumbers,
|
| 7 |
+
buildCitationResolutions,
|
| 8 |
+
citationNumberFor,
|
| 9 |
+
getMessageCitations,
|
| 10 |
+
getMessageTextContent,
|
| 11 |
+
getOrderedMessageBlocks,
|
| 12 |
+
hasRenderableContent,
|
| 13 |
+
resolveCitationHref,
|
| 14 |
+
type TutorMessage,
|
| 15 |
+
type TutorMessagePart,
|
| 16 |
+
} from "./chat-ui.ts";
|
| 17 |
+
|
| 18 |
+
function assistantMessage(
|
| 19 |
+
parts: TutorMessagePart[],
|
| 20 |
+
id = "assistant-1",
|
| 21 |
+
): TutorMessage {
|
| 22 |
+
return { id, role: "assistant", parts } as unknown as TutorMessage;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
function source(overrides: Partial<SourcePartData> = {}): SourcePartData {
|
| 26 |
+
return {
|
| 27 |
+
docId: "doc-1",
|
| 28 |
+
title: "A useful source",
|
| 29 |
+
url: "https://docs.example.test/guide",
|
| 30 |
+
sourceKey: "docs",
|
| 31 |
+
sourceLabel: "Example docs",
|
| 32 |
+
score: 0.9,
|
| 33 |
+
group: "docs",
|
| 34 |
+
...overrides,
|
| 35 |
+
};
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
test("citation numbers follow rendered-link order and ignore non-rendered links", () => {
|
| 39 |
+
const message = assistantMessage([
|
| 40 |
+
{
|
| 41 |
+
type: "text",
|
| 42 |
+
text: [
|
| 43 |
+
"[Nested [label]](https://docs.example.test/Spring_(framework)#intro)",
|
| 44 |
+
"`[inline code](https://ignored.example.test/inline)`",
|
| 45 |
+
"",
|
| 46 |
+
"```md\n[fenced](https://ignored.example.test/fenced)\n```",
|
| 47 |
+
"[Duplicate](https://docs.example.test/Spring_(framework)/)",
|
| 48 |
+
].join("\n"),
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
type: "text",
|
| 52 |
+
text: [
|
| 53 |
+
"[Encoded](https://docs.example.test/caf%C3%A9)",
|
| 54 |
+
"[Unicode duplicate](https://docs.example.test/café#heading)",
|
| 55 |
+
"[Angle form](<https://other.example.test/a(b)>)",
|
| 56 |
+
"[With title](https://third.example.test/path \"Source title\")",
|
| 57 |
+
"[Malformed](https://ignored.example.test/wrong]",
|
| 58 |
+
"[Relative](raw/docs/example.md)",
|
| 59 |
+
].join("\n"),
|
| 60 |
+
},
|
| 61 |
+
]);
|
| 62 |
+
|
| 63 |
+
const numbers = buildCitationNumbers(message);
|
| 64 |
+
|
| 65 |
+
assert.deepEqual([...numbers.entries()], [
|
| 66 |
+
["https://docs.example.test/Spring_(framework)", 1],
|
| 67 |
+
["https://docs.example.test/caf%C3%A9", 2],
|
| 68 |
+
["https://other.example.test/a(b)", 3],
|
| 69 |
+
["https://third.example.test/path", 4],
|
| 70 |
+
]);
|
| 71 |
+
assert.equal(citationNumberFor(numbers, "https://docs.example.test/café/"), 2);
|
| 72 |
+
assert.equal(citationNumberFor(numbers, "https://ignored.example.test/inline"), undefined);
|
| 73 |
+
});
|
| 74 |
+
|
| 75 |
+
test("KB references resolve to one navigable citation number", () => {
|
| 76 |
+
const canonicalUrl = "https://academy.example.test/lessons/lora";
|
| 77 |
+
const kbSource = source({
|
| 78 |
+
docId: "lora-doc",
|
| 79 |
+
path: "raw/docs/peft/lora.md",
|
| 80 |
+
url: canonicalUrl,
|
| 81 |
+
});
|
| 82 |
+
const message = assistantMessage([
|
| 83 |
+
{
|
| 84 |
+
type: "text",
|
| 85 |
+
text: [
|
| 86 |
+
"[By id](kb://doc/lora-doc)",
|
| 87 |
+
"[By relative path](./raw/docs/peft/lora.md)",
|
| 88 |
+
"[By full path](data/kb/raw/docs/peft/lora.md)",
|
| 89 |
+
"[Direct URL](https://academy.example.test/lessons/lora#examples)",
|
| 90 |
+
"[Missing KB file](raw/docs/peft/missing.md)",
|
| 91 |
+
].join(" "),
|
| 92 |
+
},
|
| 93 |
+
{ type: "data-source", data: kbSource },
|
| 94 |
+
]);
|
| 95 |
+
|
| 96 |
+
const resolutions = buildCitationResolutions(message);
|
| 97 |
+
assert.deepEqual([...resolutions.entries()], [
|
| 98 |
+
["kb://doc/lora-doc", canonicalUrl],
|
| 99 |
+
["raw/docs/peft/lora.md", canonicalUrl],
|
| 100 |
+
]);
|
| 101 |
+
assert.equal(
|
| 102 |
+
resolveCitationHref("data/kb/raw/docs/peft/lora.md", resolutions),
|
| 103 |
+
canonicalUrl,
|
| 104 |
+
);
|
| 105 |
+
assert.equal(resolveCitationHref("raw/docs/peft/missing.md", resolutions), undefined);
|
| 106 |
+
|
| 107 |
+
const numbers = buildCitationNumbers(message, resolutions);
|
| 108 |
+
assert.deepEqual([...numbers.entries()], [[canonicalUrl, 1]]);
|
| 109 |
+
assert.equal(citationNumberFor(numbers, `${canonicalUrl}/#details`), 1);
|
| 110 |
+
});
|
| 111 |
+
|
| 112 |
+
test("server-provided sources preserve order and deduplicate normalized URLs", () => {
|
| 113 |
+
const longTitle = "T".repeat(90);
|
| 114 |
+
const message = assistantMessage([
|
| 115 |
+
{
|
| 116 |
+
type: "data-source",
|
| 117 |
+
data: source({
|
| 118 |
+
title: longTitle,
|
| 119 |
+
url: "https://docs.example.test/guide/#intro",
|
| 120 |
+
group: "docs",
|
| 121 |
+
}),
|
| 122 |
+
},
|
| 123 |
+
{
|
| 124 |
+
type: "data-source",
|
| 125 |
+
data: source({
|
| 126 |
+
title: "Duplicate",
|
| 127 |
+
url: "https://docs.example.test/guide/",
|
| 128 |
+
group: "docs",
|
| 129 |
+
}),
|
| 130 |
+
},
|
| 131 |
+
{
|
| 132 |
+
type: "data-source",
|
| 133 |
+
data: source({
|
| 134 |
+
title: " — ",
|
| 135 |
+
url: "https://www.search.example.test/result",
|
| 136 |
+
sourceLabel: "Search result",
|
| 137 |
+
group: "web",
|
| 138 |
+
}),
|
| 139 |
+
},
|
| 140 |
+
{
|
| 141 |
+
type: "data-source",
|
| 142 |
+
data: source({
|
| 143 |
+
title: "Course lesson",
|
| 144 |
+
url: "https://academy.example.test/course/lesson",
|
| 145 |
+
sourceLabel: "Agent Engineering",
|
| 146 |
+
group: "courses",
|
| 147 |
+
}),
|
| 148 |
+
},
|
| 149 |
+
{ type: "data-source", data: source({ url: " " }) },
|
| 150 |
+
]);
|
| 151 |
+
|
| 152 |
+
assert.deepEqual(getMessageCitations(message), [
|
| 153 |
+
{
|
| 154 |
+
label: longTitle.slice(0, 80),
|
| 155 |
+
url: "https://docs.example.test/guide/#intro",
|
| 156 |
+
kind: "doc",
|
| 157 |
+
sublabel: "Example docs",
|
| 158 |
+
},
|
| 159 |
+
{
|
| 160 |
+
label: "search.example.test",
|
| 161 |
+
url: "https://www.search.example.test/result",
|
| 162 |
+
kind: "web",
|
| 163 |
+
sublabel: "search.example.test",
|
| 164 |
+
},
|
| 165 |
+
{
|
| 166 |
+
label: "Course lesson",
|
| 167 |
+
url: "https://academy.example.test/course/lesson",
|
| 168 |
+
kind: "course",
|
| 169 |
+
sublabel: "Agent Engineering",
|
| 170 |
+
},
|
| 171 |
+
]);
|
| 172 |
+
});
|
| 173 |
+
|
| 174 |
+
test("ordered blocks retain provider order and ignored parts do not split blocks", () => {
|
| 175 |
+
const reasoningOne = { type: "reasoning", text: "First thought" };
|
| 176 |
+
const reasoningTwo = { type: "reasoning", text: "Second fragment" };
|
| 177 |
+
const tool = {
|
| 178 |
+
type: "tool-retrieve_tutor_context",
|
| 179 |
+
toolCallId: "call-1",
|
| 180 |
+
state: "output-available",
|
| 181 |
+
};
|
| 182 |
+
const reasoningThree = { type: "reasoning", text: "After the tool" };
|
| 183 |
+
const textOne = { type: "text", text: "Answer one" };
|
| 184 |
+
const textTwo = { type: "text", text: "Answer two" };
|
| 185 |
+
const finalTool = {
|
| 186 |
+
type: "tool-web_search",
|
| 187 |
+
toolCallId: "call-2",
|
| 188 |
+
state: "input-available",
|
| 189 |
+
};
|
| 190 |
+
const message = assistantMessage([
|
| 191 |
+
{ type: "data-source", data: source() },
|
| 192 |
+
reasoningOne,
|
| 193 |
+
{ type: "data-context_stats", data: { tokens: 10 } },
|
| 194 |
+
reasoningTwo,
|
| 195 |
+
tool,
|
| 196 |
+
reasoningThree,
|
| 197 |
+
textOne,
|
| 198 |
+
{ type: "data-source", data: source() },
|
| 199 |
+
textTwo,
|
| 200 |
+
finalTool,
|
| 201 |
+
]);
|
| 202 |
+
|
| 203 |
+
assert.deepEqual(getOrderedMessageBlocks(message), [
|
| 204 |
+
{
|
| 205 |
+
key: "assistant-1-activity-0",
|
| 206 |
+
kind: "activity",
|
| 207 |
+
parts: [reasoningOne, reasoningTwo, tool, reasoningThree],
|
| 208 |
+
},
|
| 209 |
+
{
|
| 210 |
+
key: "assistant-1-text-1",
|
| 211 |
+
kind: "text",
|
| 212 |
+
parts: [textOne, textTwo],
|
| 213 |
+
},
|
| 214 |
+
{
|
| 215 |
+
key: "assistant-1-activity-2",
|
| 216 |
+
kind: "activity",
|
| 217 |
+
parts: [finalTool],
|
| 218 |
+
},
|
| 219 |
+
]);
|
| 220 |
+
});
|
| 221 |
+
|
| 222 |
+
test("activity items merge reasoning fragments but split them at tool calls", () => {
|
| 223 |
+
const tool = {
|
| 224 |
+
type: "tool-run_kb_command",
|
| 225 |
+
toolCallId: "call-1",
|
| 226 |
+
state: "output-available",
|
| 227 |
+
};
|
| 228 |
+
const parts: TutorMessagePart[] = [
|
| 229 |
+
{ type: "reasoning", text: " First fragment " },
|
| 230 |
+
{ type: "data-source", data: source() },
|
| 231 |
+
{ type: "reasoning", text: "" },
|
| 232 |
+
{ type: "reasoning", text: "Second fragment" },
|
| 233 |
+
tool,
|
| 234 |
+
{ type: "data-context_stats", data: {} },
|
| 235 |
+
{ type: "reasoning", text: "Final thought" },
|
| 236 |
+
];
|
| 237 |
+
|
| 238 |
+
assert.deepEqual(buildActivityItems(parts), [
|
| 239 |
+
{
|
| 240 |
+
kind: "reasoning",
|
| 241 |
+
key: "r-0",
|
| 242 |
+
text: "First fragment\n\nSecond fragment",
|
| 243 |
+
},
|
| 244 |
+
{ kind: "tool", key: "t-1-call-1", part: tool },
|
| 245 |
+
{ kind: "reasoning", key: "r-2", text: "Final thought" },
|
| 246 |
+
]);
|
| 247 |
+
});
|
| 248 |
+
|
| 249 |
+
test("renderability ignores empty stream placeholders but retains real activity", () => {
|
| 250 |
+
assert.equal(hasRenderableContent(assistantMessage([])), false);
|
| 251 |
+
assert.equal(
|
| 252 |
+
hasRenderableContent(assistantMessage([{ type: "data-source", data: source() }])),
|
| 253 |
+
false,
|
| 254 |
+
);
|
| 255 |
+
assert.equal(hasRenderableContent(assistantMessage([{ type: "text", text: "" }])), false);
|
| 256 |
+
assert.equal(
|
| 257 |
+
hasRenderableContent(assistantMessage([{ type: "reasoning", text: "" }])),
|
| 258 |
+
false,
|
| 259 |
+
);
|
| 260 |
+
assert.equal(
|
| 261 |
+
hasRenderableContent(assistantMessage([{ type: "reasoning", text: "Thinking" }])),
|
| 262 |
+
true,
|
| 263 |
+
);
|
| 264 |
+
assert.equal(
|
| 265 |
+
hasRenderableContent(
|
| 266 |
+
assistantMessage([{ type: "tool-web_search", state: "input-streaming" }]),
|
| 267 |
+
),
|
| 268 |
+
true,
|
| 269 |
+
);
|
| 270 |
+
});
|
| 271 |
+
|
| 272 |
+
test("message text joins trimmed non-empty text parts without reshaping content inside them", () => {
|
| 273 |
+
const message = assistantMessage([
|
| 274 |
+
{ type: "text", text: " First paragraph\n " },
|
| 275 |
+
{ type: "reasoning", text: "Not user-facing" },
|
| 276 |
+
{ type: "text", text: " " },
|
| 277 |
+
{ type: "text", text: "Second paragraph\nwith another line" },
|
| 278 |
+
]);
|
| 279 |
+
|
| 280 |
+
assert.equal(
|
| 281 |
+
getMessageTextContent(message),
|
| 282 |
+
"First paragraph\n\nSecond paragraph\nwith another line",
|
| 283 |
+
);
|
| 284 |
+
});
|
frontend/lib/chat-ui.ts
CHANGED
|
@@ -109,7 +109,14 @@ export function hasRenderableContent(message: TutorMessage): boolean {
|
|
| 109 |
continue;
|
| 110 |
}
|
| 111 |
const type = String(part.type);
|
| 112 |
-
if (type === "text" || type === "reasoning"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
return true;
|
| 114 |
}
|
| 115 |
}
|
|
|
|
| 109 |
continue;
|
| 110 |
}
|
| 111 |
const type = String(part.type);
|
| 112 |
+
if (type === "text" || type === "reasoning") {
|
| 113 |
+
const text = "text" in part ? part.text : "";
|
| 114 |
+
if (typeof text === "string" && text.trim()) {
|
| 115 |
+
return true;
|
| 116 |
+
}
|
| 117 |
+
continue;
|
| 118 |
+
}
|
| 119 |
+
if (type.startsWith("tool-")) {
|
| 120 |
return true;
|
| 121 |
}
|
| 122 |
}
|
frontend/package-lock.json
CHANGED
|
@@ -22,16 +22,29 @@
|
|
| 22 |
},
|
| 23 |
"devDependencies": {
|
| 24 |
"@eslint/eslintrc": "^3.3.1",
|
|
|
|
| 25 |
"@tailwindcss/postcss": "^4.1.17",
|
|
|
|
|
|
|
|
|
|
| 26 |
"@types/node": "^24.10.1",
|
| 27 |
"@types/react": "^19.2.2",
|
| 28 |
"@types/react-dom": "^19.2.2",
|
| 29 |
"eslint": "^9.39.4",
|
| 30 |
"eslint-config-next": "^16.2.3",
|
|
|
|
| 31 |
"tailwindcss": "^4.2.2",
|
| 32 |
-
"typescript": "^6.0.3"
|
|
|
|
| 33 |
}
|
| 34 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
"node_modules/@ai-sdk/gateway": {
|
| 36 |
"version": "3.0.143",
|
| 37 |
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.143.tgz",
|
|
@@ -109,6 +122,57 @@
|
|
| 109 |
"url": "https://github.com/sponsors/sindresorhus"
|
| 110 |
}
|
| 111 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
"node_modules/@babel/code-frame": {
|
| 113 |
"version": "7.29.7",
|
| 114 |
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
|
@@ -301,6 +365,16 @@
|
|
| 301 |
"node": ">=6.0.0"
|
| 302 |
}
|
| 303 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
"node_modules/@babel/template": {
|
| 305 |
"version": "7.29.7",
|
| 306 |
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
|
@@ -349,6 +423,159 @@
|
|
| 349 |
"node": ">=6.9.0"
|
| 350 |
}
|
| 351 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
"node_modules/@emnapi/core": {
|
| 353 |
"version": "1.10.0",
|
| 354 |
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
|
@@ -526,6 +753,24 @@
|
|
| 526 |
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
| 527 |
}
|
| 528 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 529 |
"node_modules/@humanfs/core": {
|
| 530 |
"version": "0.19.2",
|
| 531 |
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
|
@@ -1388,72 +1633,36 @@
|
|
| 1388 |
"node": ">=8.0.0"
|
| 1389 |
}
|
| 1390 |
},
|
| 1391 |
-
"node_modules/@
|
| 1392 |
-
"version": "
|
| 1393 |
-
"resolved": "https://registry.npmjs.org/@
|
| 1394 |
-
"integrity": "sha512-
|
| 1395 |
-
"dev": true,
|
| 1396 |
-
"license": "MIT"
|
| 1397 |
-
},
|
| 1398 |
-
"node_modules/@standard-schema/spec": {
|
| 1399 |
-
"version": "1.1.0",
|
| 1400 |
-
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
| 1401 |
-
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
| 1402 |
-
"license": "MIT"
|
| 1403 |
-
},
|
| 1404 |
-
"node_modules/@swc/helpers": {
|
| 1405 |
-
"version": "0.5.15",
|
| 1406 |
-
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
| 1407 |
-
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
|
| 1408 |
-
"license": "Apache-2.0",
|
| 1409 |
-
"dependencies": {
|
| 1410 |
-
"tslib": "^2.8.0"
|
| 1411 |
-
}
|
| 1412 |
-
},
|
| 1413 |
-
"node_modules/@tailwindcss/node": {
|
| 1414 |
-
"version": "4.3.2",
|
| 1415 |
-
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz",
|
| 1416 |
-
"integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==",
|
| 1417 |
"dev": true,
|
| 1418 |
"license": "MIT",
|
| 1419 |
-
"
|
| 1420 |
-
"
|
| 1421 |
-
"enhanced-resolve": "5.21.6",
|
| 1422 |
-
"jiti": "^2.7.0",
|
| 1423 |
-
"lightningcss": "1.32.0",
|
| 1424 |
-
"magic-string": "^0.30.21",
|
| 1425 |
-
"source-map-js": "^1.2.1",
|
| 1426 |
-
"tailwindcss": "4.3.2"
|
| 1427 |
}
|
| 1428 |
},
|
| 1429 |
-
"node_modules/@
|
| 1430 |
-
"version": "
|
| 1431 |
-
"resolved": "https://registry.npmjs.org/@
|
| 1432 |
-
"integrity": "sha512-
|
| 1433 |
-
"
|
| 1434 |
-
"license": "
|
| 1435 |
-
"
|
| 1436 |
-
"
|
| 1437 |
},
|
| 1438 |
-
"
|
| 1439 |
-
"
|
| 1440 |
-
|
| 1441 |
-
|
| 1442 |
-
"
|
| 1443 |
-
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2",
|
| 1444 |
-
"@tailwindcss/oxide-linux-arm64-gnu": "4.3.2",
|
| 1445 |
-
"@tailwindcss/oxide-linux-arm64-musl": "4.3.2",
|
| 1446 |
-
"@tailwindcss/oxide-linux-x64-gnu": "4.3.2",
|
| 1447 |
-
"@tailwindcss/oxide-linux-x64-musl": "4.3.2",
|
| 1448 |
-
"@tailwindcss/oxide-wasm32-wasi": "4.3.2",
|
| 1449 |
-
"@tailwindcss/oxide-win32-arm64-msvc": "4.3.2",
|
| 1450 |
-
"@tailwindcss/oxide-win32-x64-msvc": "4.3.2"
|
| 1451 |
}
|
| 1452 |
},
|
| 1453 |
-
"node_modules/@
|
| 1454 |
-
"version": "
|
| 1455 |
-
"resolved": "https://registry.npmjs.org/@
|
| 1456 |
-
"integrity": "sha512-
|
| 1457 |
"cpu": [
|
| 1458 |
"arm64"
|
| 1459 |
],
|
|
@@ -1464,13 +1673,13 @@
|
|
| 1464 |
"android"
|
| 1465 |
],
|
| 1466 |
"engines": {
|
| 1467 |
-
"node": ">=
|
| 1468 |
}
|
| 1469 |
},
|
| 1470 |
-
"node_modules/@
|
| 1471 |
-
"version": "
|
| 1472 |
-
"resolved": "https://registry.npmjs.org/@
|
| 1473 |
-
"integrity": "sha512-
|
| 1474 |
"cpu": [
|
| 1475 |
"arm64"
|
| 1476 |
],
|
|
@@ -1481,13 +1690,13 @@
|
|
| 1481 |
"darwin"
|
| 1482 |
],
|
| 1483 |
"engines": {
|
| 1484 |
-
"node": ">=
|
| 1485 |
}
|
| 1486 |
},
|
| 1487 |
-
"node_modules/@
|
| 1488 |
-
"version": "
|
| 1489 |
-
"resolved": "https://registry.npmjs.org/@
|
| 1490 |
-
"integrity": "sha512-
|
| 1491 |
"cpu": [
|
| 1492 |
"x64"
|
| 1493 |
],
|
|
@@ -1498,13 +1707,13 @@
|
|
| 1498 |
"darwin"
|
| 1499 |
],
|
| 1500 |
"engines": {
|
| 1501 |
-
"node": ">=
|
| 1502 |
}
|
| 1503 |
},
|
| 1504 |
-
"node_modules/@
|
| 1505 |
-
"version": "
|
| 1506 |
-
"resolved": "https://registry.npmjs.org/@
|
| 1507 |
-
"integrity": "sha512-
|
| 1508 |
"cpu": [
|
| 1509 |
"x64"
|
| 1510 |
],
|
|
@@ -1515,13 +1724,13 @@
|
|
| 1515 |
"freebsd"
|
| 1516 |
],
|
| 1517 |
"engines": {
|
| 1518 |
-
"node": ">=
|
| 1519 |
}
|
| 1520 |
},
|
| 1521 |
-
"node_modules/@
|
| 1522 |
-
"version": "
|
| 1523 |
-
"resolved": "https://registry.npmjs.org/@
|
| 1524 |
-
"integrity": "sha512-
|
| 1525 |
"cpu": [
|
| 1526 |
"arm"
|
| 1527 |
],
|
|
@@ -1532,13 +1741,13 @@
|
|
| 1532 |
"linux"
|
| 1533 |
],
|
| 1534 |
"engines": {
|
| 1535 |
-
"node": ">=
|
| 1536 |
}
|
| 1537 |
},
|
| 1538 |
-
"node_modules/@
|
| 1539 |
-
"version": "
|
| 1540 |
-
"resolved": "https://registry.npmjs.org/@
|
| 1541 |
-
"integrity": "sha512-
|
| 1542 |
"cpu": [
|
| 1543 |
"arm64"
|
| 1544 |
],
|
|
@@ -1552,13 +1761,13 @@
|
|
| 1552 |
"linux"
|
| 1553 |
],
|
| 1554 |
"engines": {
|
| 1555 |
-
"node": ">=
|
| 1556 |
}
|
| 1557 |
},
|
| 1558 |
-
"node_modules/@
|
| 1559 |
-
"version": "
|
| 1560 |
-
"resolved": "https://registry.npmjs.org/@
|
| 1561 |
-
"integrity": "sha512-
|
| 1562 |
"cpu": [
|
| 1563 |
"arm64"
|
| 1564 |
],
|
|
@@ -1572,15 +1781,393 @@
|
|
| 1572 |
"linux"
|
| 1573 |
],
|
| 1574 |
"engines": {
|
| 1575 |
-
"node": ">=
|
| 1576 |
}
|
| 1577 |
},
|
| 1578 |
-
"node_modules/@
|
| 1579 |
-
"version": "
|
| 1580 |
-
"resolved": "https://registry.npmjs.org/@
|
| 1581 |
-
"integrity": "sha512-
|
| 1582 |
"cpu": [
|
| 1583 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1584 |
],
|
| 1585 |
"dev": true,
|
| 1586 |
"libc": [
|
|
@@ -1693,6 +2280,107 @@
|
|
| 1693 |
"tailwindcss": "4.3.2"
|
| 1694 |
}
|
| 1695 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1696 |
"node_modules/@tybys/wasm-util": {
|
| 1697 |
"version": "0.10.3",
|
| 1698 |
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
|
|
@@ -1704,6 +2392,25 @@
|
|
| 1704 |
"tslib": "^2.4.0"
|
| 1705 |
}
|
| 1706 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1707 |
"node_modules/@types/debug": {
|
| 1708 |
"version": "4.1.13",
|
| 1709 |
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
|
|
@@ -1713,6 +2420,13 @@
|
|
| 1713 |
"@types/ms": "*"
|
| 1714 |
}
|
| 1715 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1716 |
"node_modules/@types/estree": {
|
| 1717 |
"version": "1.0.9",
|
| 1718 |
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
|
@@ -2428,41 +3142,154 @@
|
|
| 2428 |
"win32"
|
| 2429 |
]
|
| 2430 |
},
|
| 2431 |
-
"node_modules/@unrs/resolver-binding-win32-ia32-msvc": {
|
| 2432 |
-
"version": "1.12.2",
|
| 2433 |
-
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz",
|
| 2434 |
-
"integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==",
|
| 2435 |
-
"cpu": [
|
| 2436 |
-
"ia32"
|
| 2437 |
-
],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2438 |
"dev": true,
|
| 2439 |
"license": "MIT",
|
| 2440 |
-
"
|
| 2441 |
-
|
| 2442 |
-
"
|
| 2443 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2444 |
},
|
| 2445 |
-
"node_modules/@
|
| 2446 |
-
"version": "1.
|
| 2447 |
-
"resolved": "https://registry.npmjs.org/@
|
| 2448 |
-
"integrity": "sha512-
|
| 2449 |
-
"cpu": [
|
| 2450 |
-
"x64"
|
| 2451 |
-
],
|
| 2452 |
"dev": true,
|
| 2453 |
"license": "MIT",
|
| 2454 |
-
"
|
| 2455 |
-
|
| 2456 |
-
|
| 2457 |
-
]
|
| 2458 |
},
|
| 2459 |
-
"node_modules/@
|
| 2460 |
-
"version": "
|
| 2461 |
-
"resolved": "https://registry.npmjs.org/@
|
| 2462 |
-
"integrity": "sha512-
|
| 2463 |
-
"
|
| 2464 |
-
"
|
| 2465 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2466 |
}
|
| 2467 |
},
|
| 2468 |
"node_modules/acorn": {
|
|
@@ -2523,6 +3350,17 @@
|
|
| 2523 |
"url": "https://github.com/sponsors/epoberezkin"
|
| 2524 |
}
|
| 2525 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2526 |
"node_modules/ansi-styles": {
|
| 2527 |
"version": "4.3.0",
|
| 2528 |
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
|
@@ -2716,6 +3554,16 @@
|
|
| 2716 |
"url": "https://github.com/sponsors/ljharb"
|
| 2717 |
}
|
| 2718 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2719 |
"node_modules/ast-types-flow": {
|
| 2720 |
"version": "0.0.8",
|
| 2721 |
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
|
|
@@ -2798,6 +3646,16 @@
|
|
| 2798 |
"node": ">=6.0.0"
|
| 2799 |
}
|
| 2800 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2801 |
"node_modules/brace-expansion": {
|
| 2802 |
"version": "1.1.15",
|
| 2803 |
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
|
|
@@ -2946,6 +3804,16 @@
|
|
| 2946 |
"url": "https://github.com/sponsors/wooorm"
|
| 2947 |
}
|
| 2948 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2949 |
"node_modules/chalk": {
|
| 2950 |
"version": "4.1.2",
|
| 2951 |
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
|
@@ -3077,6 +3945,27 @@
|
|
| 3077 |
"node": ">= 8"
|
| 3078 |
}
|
| 3079 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3080 |
"node_modules/csstype": {
|
| 3081 |
"version": "3.2.3",
|
| 3082 |
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
|
@@ -3090,6 +3979,20 @@
|
|
| 3090 |
"dev": true,
|
| 3091 |
"license": "BSD-2-Clause"
|
| 3092 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3093 |
"node_modules/data-view-buffer": {
|
| 3094 |
"version": "1.0.2",
|
| 3095 |
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
|
|
@@ -3161,6 +4064,13 @@
|
|
| 3161 |
}
|
| 3162 |
}
|
| 3163 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3164 |
"node_modules/decode-named-character-reference": {
|
| 3165 |
"version": "1.3.0",
|
| 3166 |
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
|
|
@@ -3262,6 +4172,14 @@
|
|
| 3262 |
"node": ">=0.10.0"
|
| 3263 |
}
|
| 3264 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3265 |
"node_modules/dunder-proto": {
|
| 3266 |
"version": "1.0.1",
|
| 3267 |
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
|
@@ -3305,6 +4223,19 @@
|
|
| 3305 |
"node": ">=10.13.0"
|
| 3306 |
}
|
| 3307 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3308 |
"node_modules/es-abstract": {
|
| 3309 |
"version": "1.24.2",
|
| 3310 |
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz",
|
|
@@ -3441,6 +4372,13 @@
|
|
| 3441 |
"node": ">= 0.4"
|
| 3442 |
}
|
| 3443 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3444 |
"node_modules/es-object-atoms": {
|
| 3445 |
"version": "1.1.2",
|
| 3446 |
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
|
@@ -3933,6 +4871,16 @@
|
|
| 3933 |
"url": "https://opencollective.com/unified"
|
| 3934 |
}
|
| 3935 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3936 |
"node_modules/esutils": {
|
| 3937 |
"version": "2.0.3",
|
| 3938 |
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
|
@@ -3952,6 +4900,16 @@
|
|
| 3952 |
"node": ">=18.0.0"
|
| 3953 |
}
|
| 3954 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3955 |
"node_modules/extend": {
|
| 3956 |
"version": "3.0.2",
|
| 3957 |
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
|
@@ -4099,6 +5057,21 @@
|
|
| 4099 |
"url": "https://github.com/sponsors/ljharb"
|
| 4100 |
}
|
| 4101 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4102 |
"node_modules/function-bind": {
|
| 4103 |
"version": "1.1.2",
|
| 4104 |
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
|
@@ -4485,6 +5458,19 @@
|
|
| 4485 |
"node": ">=12.0.0"
|
| 4486 |
}
|
| 4487 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4488 |
"node_modules/html-url-attributes": {
|
| 4489 |
"version": "3.0.1",
|
| 4490 |
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
|
|
@@ -4532,6 +5518,16 @@
|
|
| 4532 |
"node": ">=0.8.19"
|
| 4533 |
}
|
| 4534 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4535 |
"node_modules/inline-style-parser": {
|
| 4536 |
"version": "0.2.7",
|
| 4537 |
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
|
|
@@ -4895,6 +5891,13 @@
|
|
| 4895 |
"url": "https://github.com/sponsors/sindresorhus"
|
| 4896 |
}
|
| 4897 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4898 |
"node_modules/is-regex": {
|
| 4899 |
"version": "1.2.1",
|
| 4900 |
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
|
@@ -5112,6 +6115,57 @@
|
|
| 5112 |
"js-yaml": "bin/js-yaml.js"
|
| 5113 |
}
|
| 5114 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5115 |
"node_modules/jsesc": {
|
| 5116 |
"version": "3.1.0",
|
| 5117 |
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
|
@@ -5578,6 +6632,17 @@
|
|
| 5578 |
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
| 5579 |
}
|
| 5580 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5581 |
"node_modules/magic-string": {
|
| 5582 |
"version": "0.30.21",
|
| 5583 |
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
|
@@ -5890,6 +6955,13 @@
|
|
| 5890 |
"url": "https://opencollective.com/unified"
|
| 5891 |
}
|
| 5892 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5893 |
"node_modules/merge2": {
|
| 5894 |
"version": "1.4.1",
|
| 5895 |
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
|
@@ -6477,6 +7549,16 @@
|
|
| 6477 |
"node": ">=8.6"
|
| 6478 |
}
|
| 6479 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6480 |
"node_modules/minimatch": {
|
| 6481 |
"version": "3.1.5",
|
| 6482 |
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
|
@@ -6780,6 +7862,20 @@
|
|
| 6780 |
"url": "https://github.com/sponsors/ljharb"
|
| 6781 |
}
|
| 6782 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6783 |
"node_modules/optionator": {
|
| 6784 |
"version": "0.9.4",
|
| 6785 |
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
|
@@ -6886,6 +7982,19 @@
|
|
| 6886 |
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
|
| 6887 |
"license": "MIT"
|
| 6888 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6889 |
"node_modules/path-exists": {
|
| 6890 |
"version": "4.0.0",
|
| 6891 |
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
|
@@ -6913,6 +8022,13 @@
|
|
| 6913 |
"dev": true,
|
| 6914 |
"license": "MIT"
|
| 6915 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6916 |
"node_modules/picocolors": {
|
| 6917 |
"version": "1.1.1",
|
| 6918 |
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
|
@@ -6932,6 +8048,52 @@
|
|
| 6932 |
"url": "https://github.com/sponsors/jonschlinkert"
|
| 6933 |
}
|
| 6934 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6935 |
"node_modules/possible-typed-array-names": {
|
| 6936 |
"version": "1.1.0",
|
| 6937 |
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
|
@@ -6943,9 +8105,9 @@
|
|
| 6943 |
}
|
| 6944 |
},
|
| 6945 |
"node_modules/postcss": {
|
| 6946 |
-
"version": "8.5.
|
| 6947 |
-
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.
|
| 6948 |
-
"integrity": "sha512-
|
| 6949 |
"dev": true,
|
| 6950 |
"funding": [
|
| 6951 |
{
|
|
@@ -6981,6 +8143,44 @@
|
|
| 6981 |
"node": ">= 0.8.0"
|
| 6982 |
}
|
| 6983 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6984 |
"node_modules/prop-types": {
|
| 6985 |
"version": "15.8.1",
|
| 6986 |
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
|
@@ -7089,6 +8289,20 @@
|
|
| 7089 |
"react": ">=18"
|
| 7090 |
}
|
| 7091 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7092 |
"node_modules/reflect.getprototypeof": {
|
| 7093 |
"version": "1.0.10",
|
| 7094 |
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
|
@@ -7216,6 +8430,16 @@
|
|
| 7216 |
"url": "https://opencollective.com/unified"
|
| 7217 |
}
|
| 7218 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7219 |
"node_modules/resolve": {
|
| 7220 |
"version": "2.0.0-next.7",
|
| 7221 |
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz",
|
|
@@ -7271,6 +8495,40 @@
|
|
| 7271 |
"node": ">=0.10.0"
|
| 7272 |
}
|
| 7273 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7274 |
"node_modules/run-parallel": {
|
| 7275 |
"version": "1.2.0",
|
| 7276 |
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
|
@@ -7350,6 +8608,19 @@
|
|
| 7350 |
"url": "https://github.com/sponsors/ljharb"
|
| 7351 |
}
|
| 7352 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7353 |
"node_modules/scheduler": {
|
| 7354 |
"version": "0.27.0",
|
| 7355 |
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
|
@@ -7572,6 +8843,13 @@
|
|
| 7572 |
"url": "https://github.com/sponsors/ljharb"
|
| 7573 |
}
|
| 7574 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7575 |
"node_modules/source-map-js": {
|
| 7576 |
"version": "1.2.1",
|
| 7577 |
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
|
@@ -7598,6 +8876,20 @@
|
|
| 7598 |
"dev": true,
|
| 7599 |
"license": "MIT"
|
| 7600 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7601 |
"node_modules/stop-iteration-iterator": {
|
| 7602 |
"version": "1.1.0",
|
| 7603 |
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
|
@@ -7750,6 +9042,19 @@
|
|
| 7750 |
"node": ">=4"
|
| 7751 |
}
|
| 7752 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7753 |
"node_modules/strip-json-comments": {
|
| 7754 |
"version": "3.1.1",
|
| 7755 |
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
|
@@ -7843,6 +9148,13 @@
|
|
| 7843 |
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
| 7844 |
}
|
| 7845 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7846 |
"node_modules/tailwindcss": {
|
| 7847 |
"version": "4.3.2",
|
| 7848 |
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz",
|
|
@@ -7876,6 +9188,23 @@
|
|
| 7876 |
"url": "https://github.com/sponsors/sindresorhus"
|
| 7877 |
}
|
| 7878 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7879 |
"node_modules/tinyglobby": {
|
| 7880 |
"version": "0.2.17",
|
| 7881 |
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
|
@@ -7924,6 +9253,36 @@
|
|
| 7924 |
"url": "https://github.com/sponsors/jonschlinkert"
|
| 7925 |
}
|
| 7926 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7927 |
"node_modules/to-regex-range": {
|
| 7928 |
"version": "5.0.1",
|
| 7929 |
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
|
@@ -7937,6 +9296,32 @@
|
|
| 7937 |
"node": ">=8.0"
|
| 7938 |
}
|
| 7939 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7940 |
"node_modules/trim-lines": {
|
| 7941 |
"version": "3.0.1",
|
| 7942 |
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
|
|
@@ -8150,6 +9535,16 @@
|
|
| 8150 |
"url": "https://github.com/sponsors/ljharb"
|
| 8151 |
}
|
| 8152 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8153 |
"node_modules/undici-types": {
|
| 8154 |
"version": "7.18.2",
|
| 8155 |
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
|
@@ -8374,6 +9769,248 @@
|
|
| 8374 |
"url": "https://opencollective.com/unified"
|
| 8375 |
}
|
| 8376 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8377 |
"node_modules/which": {
|
| 8378 |
"version": "2.0.2",
|
| 8379 |
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
|
@@ -8479,6 +10116,23 @@
|
|
| 8479 |
"url": "https://github.com/sponsors/ljharb"
|
| 8480 |
}
|
| 8481 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8482 |
"node_modules/word-wrap": {
|
| 8483 |
"version": "1.2.5",
|
| 8484 |
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
|
@@ -8489,6 +10143,23 @@
|
|
| 8489 |
"node": ">=0.10.0"
|
| 8490 |
}
|
| 8491 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8492 |
"node_modules/yallist": {
|
| 8493 |
"version": "3.1.1",
|
| 8494 |
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
|
|
|
| 22 |
},
|
| 23 |
"devDependencies": {
|
| 24 |
"@eslint/eslintrc": "^3.3.1",
|
| 25 |
+
"@playwright/test": "^1.61.1",
|
| 26 |
"@tailwindcss/postcss": "^4.1.17",
|
| 27 |
+
"@testing-library/jest-dom": "^6.9.1",
|
| 28 |
+
"@testing-library/react": "^16.3.2",
|
| 29 |
+
"@testing-library/user-event": "^14.6.1",
|
| 30 |
"@types/node": "^24.10.1",
|
| 31 |
"@types/react": "^19.2.2",
|
| 32 |
"@types/react-dom": "^19.2.2",
|
| 33 |
"eslint": "^9.39.4",
|
| 34 |
"eslint-config-next": "^16.2.3",
|
| 35 |
+
"jsdom": "^29.1.1",
|
| 36 |
"tailwindcss": "^4.2.2",
|
| 37 |
+
"typescript": "^6.0.3",
|
| 38 |
+
"vitest": "^4.1.10"
|
| 39 |
}
|
| 40 |
},
|
| 41 |
+
"node_modules/@adobe/css-tools": {
|
| 42 |
+
"version": "4.5.0",
|
| 43 |
+
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz",
|
| 44 |
+
"integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==",
|
| 45 |
+
"dev": true,
|
| 46 |
+
"license": "MIT"
|
| 47 |
+
},
|
| 48 |
"node_modules/@ai-sdk/gateway": {
|
| 49 |
"version": "3.0.143",
|
| 50 |
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.143.tgz",
|
|
|
|
| 122 |
"url": "https://github.com/sponsors/sindresorhus"
|
| 123 |
}
|
| 124 |
},
|
| 125 |
+
"node_modules/@asamuzakjp/css-color": {
|
| 126 |
+
"version": "5.1.11",
|
| 127 |
+
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
|
| 128 |
+
"integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==",
|
| 129 |
+
"dev": true,
|
| 130 |
+
"license": "MIT",
|
| 131 |
+
"dependencies": {
|
| 132 |
+
"@asamuzakjp/generational-cache": "^1.0.1",
|
| 133 |
+
"@csstools/css-calc": "^3.2.0",
|
| 134 |
+
"@csstools/css-color-parser": "^4.1.0",
|
| 135 |
+
"@csstools/css-parser-algorithms": "^4.0.0",
|
| 136 |
+
"@csstools/css-tokenizer": "^4.0.0"
|
| 137 |
+
},
|
| 138 |
+
"engines": {
|
| 139 |
+
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
| 140 |
+
}
|
| 141 |
+
},
|
| 142 |
+
"node_modules/@asamuzakjp/dom-selector": {
|
| 143 |
+
"version": "7.1.1",
|
| 144 |
+
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz",
|
| 145 |
+
"integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==",
|
| 146 |
+
"dev": true,
|
| 147 |
+
"license": "MIT",
|
| 148 |
+
"dependencies": {
|
| 149 |
+
"@asamuzakjp/generational-cache": "^1.0.1",
|
| 150 |
+
"@asamuzakjp/nwsapi": "^2.3.9",
|
| 151 |
+
"bidi-js": "^1.0.3",
|
| 152 |
+
"css-tree": "^3.2.1",
|
| 153 |
+
"is-potential-custom-element-name": "^1.0.1"
|
| 154 |
+
},
|
| 155 |
+
"engines": {
|
| 156 |
+
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
| 157 |
+
}
|
| 158 |
+
},
|
| 159 |
+
"node_modules/@asamuzakjp/generational-cache": {
|
| 160 |
+
"version": "1.0.1",
|
| 161 |
+
"resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz",
|
| 162 |
+
"integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==",
|
| 163 |
+
"dev": true,
|
| 164 |
+
"license": "MIT",
|
| 165 |
+
"engines": {
|
| 166 |
+
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
| 167 |
+
}
|
| 168 |
+
},
|
| 169 |
+
"node_modules/@asamuzakjp/nwsapi": {
|
| 170 |
+
"version": "2.3.9",
|
| 171 |
+
"resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
|
| 172 |
+
"integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
|
| 173 |
+
"dev": true,
|
| 174 |
+
"license": "MIT"
|
| 175 |
+
},
|
| 176 |
"node_modules/@babel/code-frame": {
|
| 177 |
"version": "7.29.7",
|
| 178 |
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
|
|
|
| 365 |
"node": ">=6.0.0"
|
| 366 |
}
|
| 367 |
},
|
| 368 |
+
"node_modules/@babel/runtime": {
|
| 369 |
+
"version": "7.29.7",
|
| 370 |
+
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
| 371 |
+
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
| 372 |
+
"dev": true,
|
| 373 |
+
"license": "MIT",
|
| 374 |
+
"engines": {
|
| 375 |
+
"node": ">=6.9.0"
|
| 376 |
+
}
|
| 377 |
+
},
|
| 378 |
"node_modules/@babel/template": {
|
| 379 |
"version": "7.29.7",
|
| 380 |
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
|
|
|
| 423 |
"node": ">=6.9.0"
|
| 424 |
}
|
| 425 |
},
|
| 426 |
+
"node_modules/@bramus/specificity": {
|
| 427 |
+
"version": "2.4.2",
|
| 428 |
+
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
|
| 429 |
+
"integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
|
| 430 |
+
"dev": true,
|
| 431 |
+
"license": "MIT",
|
| 432 |
+
"dependencies": {
|
| 433 |
+
"css-tree": "^3.0.0"
|
| 434 |
+
},
|
| 435 |
+
"bin": {
|
| 436 |
+
"specificity": "bin/cli.js"
|
| 437 |
+
}
|
| 438 |
+
},
|
| 439 |
+
"node_modules/@csstools/color-helpers": {
|
| 440 |
+
"version": "6.1.0",
|
| 441 |
+
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz",
|
| 442 |
+
"integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==",
|
| 443 |
+
"dev": true,
|
| 444 |
+
"funding": [
|
| 445 |
+
{
|
| 446 |
+
"type": "github",
|
| 447 |
+
"url": "https://github.com/sponsors/csstools"
|
| 448 |
+
},
|
| 449 |
+
{
|
| 450 |
+
"type": "opencollective",
|
| 451 |
+
"url": "https://opencollective.com/csstools"
|
| 452 |
+
}
|
| 453 |
+
],
|
| 454 |
+
"license": "MIT-0",
|
| 455 |
+
"engines": {
|
| 456 |
+
"node": ">=20.19.0"
|
| 457 |
+
}
|
| 458 |
+
},
|
| 459 |
+
"node_modules/@csstools/css-calc": {
|
| 460 |
+
"version": "3.2.1",
|
| 461 |
+
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz",
|
| 462 |
+
"integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==",
|
| 463 |
+
"dev": true,
|
| 464 |
+
"funding": [
|
| 465 |
+
{
|
| 466 |
+
"type": "github",
|
| 467 |
+
"url": "https://github.com/sponsors/csstools"
|
| 468 |
+
},
|
| 469 |
+
{
|
| 470 |
+
"type": "opencollective",
|
| 471 |
+
"url": "https://opencollective.com/csstools"
|
| 472 |
+
}
|
| 473 |
+
],
|
| 474 |
+
"license": "MIT",
|
| 475 |
+
"engines": {
|
| 476 |
+
"node": ">=20.19.0"
|
| 477 |
+
},
|
| 478 |
+
"peerDependencies": {
|
| 479 |
+
"@csstools/css-parser-algorithms": "^4.0.0",
|
| 480 |
+
"@csstools/css-tokenizer": "^4.0.0"
|
| 481 |
+
}
|
| 482 |
+
},
|
| 483 |
+
"node_modules/@csstools/css-color-parser": {
|
| 484 |
+
"version": "4.1.9",
|
| 485 |
+
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz",
|
| 486 |
+
"integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==",
|
| 487 |
+
"dev": true,
|
| 488 |
+
"funding": [
|
| 489 |
+
{
|
| 490 |
+
"type": "github",
|
| 491 |
+
"url": "https://github.com/sponsors/csstools"
|
| 492 |
+
},
|
| 493 |
+
{
|
| 494 |
+
"type": "opencollective",
|
| 495 |
+
"url": "https://opencollective.com/csstools"
|
| 496 |
+
}
|
| 497 |
+
],
|
| 498 |
+
"license": "MIT",
|
| 499 |
+
"dependencies": {
|
| 500 |
+
"@csstools/color-helpers": "^6.1.0",
|
| 501 |
+
"@csstools/css-calc": "^3.2.1"
|
| 502 |
+
},
|
| 503 |
+
"engines": {
|
| 504 |
+
"node": ">=20.19.0"
|
| 505 |
+
},
|
| 506 |
+
"peerDependencies": {
|
| 507 |
+
"@csstools/css-parser-algorithms": "^4.0.0",
|
| 508 |
+
"@csstools/css-tokenizer": "^4.0.0"
|
| 509 |
+
}
|
| 510 |
+
},
|
| 511 |
+
"node_modules/@csstools/css-parser-algorithms": {
|
| 512 |
+
"version": "4.0.0",
|
| 513 |
+
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
|
| 514 |
+
"integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
|
| 515 |
+
"dev": true,
|
| 516 |
+
"funding": [
|
| 517 |
+
{
|
| 518 |
+
"type": "github",
|
| 519 |
+
"url": "https://github.com/sponsors/csstools"
|
| 520 |
+
},
|
| 521 |
+
{
|
| 522 |
+
"type": "opencollective",
|
| 523 |
+
"url": "https://opencollective.com/csstools"
|
| 524 |
+
}
|
| 525 |
+
],
|
| 526 |
+
"license": "MIT",
|
| 527 |
+
"engines": {
|
| 528 |
+
"node": ">=20.19.0"
|
| 529 |
+
},
|
| 530 |
+
"peerDependencies": {
|
| 531 |
+
"@csstools/css-tokenizer": "^4.0.0"
|
| 532 |
+
}
|
| 533 |
+
},
|
| 534 |
+
"node_modules/@csstools/css-syntax-patches-for-csstree": {
|
| 535 |
+
"version": "1.1.6",
|
| 536 |
+
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz",
|
| 537 |
+
"integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==",
|
| 538 |
+
"dev": true,
|
| 539 |
+
"funding": [
|
| 540 |
+
{
|
| 541 |
+
"type": "github",
|
| 542 |
+
"url": "https://github.com/sponsors/csstools"
|
| 543 |
+
},
|
| 544 |
+
{
|
| 545 |
+
"type": "opencollective",
|
| 546 |
+
"url": "https://opencollective.com/csstools"
|
| 547 |
+
}
|
| 548 |
+
],
|
| 549 |
+
"license": "MIT-0",
|
| 550 |
+
"peerDependencies": {
|
| 551 |
+
"css-tree": "^3.2.1"
|
| 552 |
+
},
|
| 553 |
+
"peerDependenciesMeta": {
|
| 554 |
+
"css-tree": {
|
| 555 |
+
"optional": true
|
| 556 |
+
}
|
| 557 |
+
}
|
| 558 |
+
},
|
| 559 |
+
"node_modules/@csstools/css-tokenizer": {
|
| 560 |
+
"version": "4.0.0",
|
| 561 |
+
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
|
| 562 |
+
"integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
|
| 563 |
+
"dev": true,
|
| 564 |
+
"funding": [
|
| 565 |
+
{
|
| 566 |
+
"type": "github",
|
| 567 |
+
"url": "https://github.com/sponsors/csstools"
|
| 568 |
+
},
|
| 569 |
+
{
|
| 570 |
+
"type": "opencollective",
|
| 571 |
+
"url": "https://opencollective.com/csstools"
|
| 572 |
+
}
|
| 573 |
+
],
|
| 574 |
+
"license": "MIT",
|
| 575 |
+
"engines": {
|
| 576 |
+
"node": ">=20.19.0"
|
| 577 |
+
}
|
| 578 |
+
},
|
| 579 |
"node_modules/@emnapi/core": {
|
| 580 |
"version": "1.10.0",
|
| 581 |
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
|
|
|
| 753 |
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
| 754 |
}
|
| 755 |
},
|
| 756 |
+
"node_modules/@exodus/bytes": {
|
| 757 |
+
"version": "1.15.1",
|
| 758 |
+
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz",
|
| 759 |
+
"integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==",
|
| 760 |
+
"dev": true,
|
| 761 |
+
"license": "MIT",
|
| 762 |
+
"engines": {
|
| 763 |
+
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
| 764 |
+
},
|
| 765 |
+
"peerDependencies": {
|
| 766 |
+
"@noble/hashes": "^1.8.0 || ^2.0.0"
|
| 767 |
+
},
|
| 768 |
+
"peerDependenciesMeta": {
|
| 769 |
+
"@noble/hashes": {
|
| 770 |
+
"optional": true
|
| 771 |
+
}
|
| 772 |
+
}
|
| 773 |
+
},
|
| 774 |
"node_modules/@humanfs/core": {
|
| 775 |
"version": "0.19.2",
|
| 776 |
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
|
|
|
| 1633 |
"node": ">=8.0.0"
|
| 1634 |
}
|
| 1635 |
},
|
| 1636 |
+
"node_modules/@oxc-project/types": {
|
| 1637 |
+
"version": "0.139.0",
|
| 1638 |
+
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
|
| 1639 |
+
"integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1640 |
"dev": true,
|
| 1641 |
"license": "MIT",
|
| 1642 |
+
"funding": {
|
| 1643 |
+
"url": "https://github.com/sponsors/Boshen"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1644 |
}
|
| 1645 |
},
|
| 1646 |
+
"node_modules/@playwright/test": {
|
| 1647 |
+
"version": "1.61.1",
|
| 1648 |
+
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
|
| 1649 |
+
"integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
|
| 1650 |
+
"devOptional": true,
|
| 1651 |
+
"license": "Apache-2.0",
|
| 1652 |
+
"dependencies": {
|
| 1653 |
+
"playwright": "1.61.1"
|
| 1654 |
},
|
| 1655 |
+
"bin": {
|
| 1656 |
+
"playwright": "cli.js"
|
| 1657 |
+
},
|
| 1658 |
+
"engines": {
|
| 1659 |
+
"node": ">=18"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1660 |
}
|
| 1661 |
},
|
| 1662 |
+
"node_modules/@rolldown/binding-android-arm64": {
|
| 1663 |
+
"version": "1.1.5",
|
| 1664 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
|
| 1665 |
+
"integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
|
| 1666 |
"cpu": [
|
| 1667 |
"arm64"
|
| 1668 |
],
|
|
|
|
| 1673 |
"android"
|
| 1674 |
],
|
| 1675 |
"engines": {
|
| 1676 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 1677 |
}
|
| 1678 |
},
|
| 1679 |
+
"node_modules/@rolldown/binding-darwin-arm64": {
|
| 1680 |
+
"version": "1.1.5",
|
| 1681 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
|
| 1682 |
+
"integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
|
| 1683 |
"cpu": [
|
| 1684 |
"arm64"
|
| 1685 |
],
|
|
|
|
| 1690 |
"darwin"
|
| 1691 |
],
|
| 1692 |
"engines": {
|
| 1693 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 1694 |
}
|
| 1695 |
},
|
| 1696 |
+
"node_modules/@rolldown/binding-darwin-x64": {
|
| 1697 |
+
"version": "1.1.5",
|
| 1698 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
|
| 1699 |
+
"integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
|
| 1700 |
"cpu": [
|
| 1701 |
"x64"
|
| 1702 |
],
|
|
|
|
| 1707 |
"darwin"
|
| 1708 |
],
|
| 1709 |
"engines": {
|
| 1710 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 1711 |
}
|
| 1712 |
},
|
| 1713 |
+
"node_modules/@rolldown/binding-freebsd-x64": {
|
| 1714 |
+
"version": "1.1.5",
|
| 1715 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
|
| 1716 |
+
"integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
|
| 1717 |
"cpu": [
|
| 1718 |
"x64"
|
| 1719 |
],
|
|
|
|
| 1724 |
"freebsd"
|
| 1725 |
],
|
| 1726 |
"engines": {
|
| 1727 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 1728 |
}
|
| 1729 |
},
|
| 1730 |
+
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
| 1731 |
+
"version": "1.1.5",
|
| 1732 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
|
| 1733 |
+
"integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
|
| 1734 |
"cpu": [
|
| 1735 |
"arm"
|
| 1736 |
],
|
|
|
|
| 1741 |
"linux"
|
| 1742 |
],
|
| 1743 |
"engines": {
|
| 1744 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 1745 |
}
|
| 1746 |
},
|
| 1747 |
+
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
| 1748 |
+
"version": "1.1.5",
|
| 1749 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
|
| 1750 |
+
"integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
|
| 1751 |
"cpu": [
|
| 1752 |
"arm64"
|
| 1753 |
],
|
|
|
|
| 1761 |
"linux"
|
| 1762 |
],
|
| 1763 |
"engines": {
|
| 1764 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 1765 |
}
|
| 1766 |
},
|
| 1767 |
+
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
| 1768 |
+
"version": "1.1.5",
|
| 1769 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
|
| 1770 |
+
"integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
|
| 1771 |
"cpu": [
|
| 1772 |
"arm64"
|
| 1773 |
],
|
|
|
|
| 1781 |
"linux"
|
| 1782 |
],
|
| 1783 |
"engines": {
|
| 1784 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 1785 |
}
|
| 1786 |
},
|
| 1787 |
+
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
| 1788 |
+
"version": "1.1.5",
|
| 1789 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
|
| 1790 |
+
"integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
|
| 1791 |
"cpu": [
|
| 1792 |
+
"ppc64"
|
| 1793 |
+
],
|
| 1794 |
+
"dev": true,
|
| 1795 |
+
"libc": [
|
| 1796 |
+
"glibc"
|
| 1797 |
+
],
|
| 1798 |
+
"license": "MIT",
|
| 1799 |
+
"optional": true,
|
| 1800 |
+
"os": [
|
| 1801 |
+
"linux"
|
| 1802 |
+
],
|
| 1803 |
+
"engines": {
|
| 1804 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 1805 |
+
}
|
| 1806 |
+
},
|
| 1807 |
+
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
| 1808 |
+
"version": "1.1.5",
|
| 1809 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
|
| 1810 |
+
"integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
|
| 1811 |
+
"cpu": [
|
| 1812 |
+
"s390x"
|
| 1813 |
+
],
|
| 1814 |
+
"dev": true,
|
| 1815 |
+
"libc": [
|
| 1816 |
+
"glibc"
|
| 1817 |
+
],
|
| 1818 |
+
"license": "MIT",
|
| 1819 |
+
"optional": true,
|
| 1820 |
+
"os": [
|
| 1821 |
+
"linux"
|
| 1822 |
+
],
|
| 1823 |
+
"engines": {
|
| 1824 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 1825 |
+
}
|
| 1826 |
+
},
|
| 1827 |
+
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
| 1828 |
+
"version": "1.1.5",
|
| 1829 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
|
| 1830 |
+
"integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
|
| 1831 |
+
"cpu": [
|
| 1832 |
+
"x64"
|
| 1833 |
+
],
|
| 1834 |
+
"dev": true,
|
| 1835 |
+
"libc": [
|
| 1836 |
+
"glibc"
|
| 1837 |
+
],
|
| 1838 |
+
"license": "MIT",
|
| 1839 |
+
"optional": true,
|
| 1840 |
+
"os": [
|
| 1841 |
+
"linux"
|
| 1842 |
+
],
|
| 1843 |
+
"engines": {
|
| 1844 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 1845 |
+
}
|
| 1846 |
+
},
|
| 1847 |
+
"node_modules/@rolldown/binding-linux-x64-musl": {
|
| 1848 |
+
"version": "1.1.5",
|
| 1849 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
|
| 1850 |
+
"integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
|
| 1851 |
+
"cpu": [
|
| 1852 |
+
"x64"
|
| 1853 |
+
],
|
| 1854 |
+
"dev": true,
|
| 1855 |
+
"libc": [
|
| 1856 |
+
"musl"
|
| 1857 |
+
],
|
| 1858 |
+
"license": "MIT",
|
| 1859 |
+
"optional": true,
|
| 1860 |
+
"os": [
|
| 1861 |
+
"linux"
|
| 1862 |
+
],
|
| 1863 |
+
"engines": {
|
| 1864 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 1865 |
+
}
|
| 1866 |
+
},
|
| 1867 |
+
"node_modules/@rolldown/binding-openharmony-arm64": {
|
| 1868 |
+
"version": "1.1.5",
|
| 1869 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
|
| 1870 |
+
"integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
|
| 1871 |
+
"cpu": [
|
| 1872 |
+
"arm64"
|
| 1873 |
+
],
|
| 1874 |
+
"dev": true,
|
| 1875 |
+
"license": "MIT",
|
| 1876 |
+
"optional": true,
|
| 1877 |
+
"os": [
|
| 1878 |
+
"openharmony"
|
| 1879 |
+
],
|
| 1880 |
+
"engines": {
|
| 1881 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 1882 |
+
}
|
| 1883 |
+
},
|
| 1884 |
+
"node_modules/@rolldown/binding-wasm32-wasi": {
|
| 1885 |
+
"version": "1.1.5",
|
| 1886 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
|
| 1887 |
+
"integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
|
| 1888 |
+
"cpu": [
|
| 1889 |
+
"wasm32"
|
| 1890 |
+
],
|
| 1891 |
+
"dev": true,
|
| 1892 |
+
"license": "MIT",
|
| 1893 |
+
"optional": true,
|
| 1894 |
+
"dependencies": {
|
| 1895 |
+
"@emnapi/core": "1.11.1",
|
| 1896 |
+
"@emnapi/runtime": "1.11.1",
|
| 1897 |
+
"@napi-rs/wasm-runtime": "^1.1.6"
|
| 1898 |
+
},
|
| 1899 |
+
"engines": {
|
| 1900 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 1901 |
+
}
|
| 1902 |
+
},
|
| 1903 |
+
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": {
|
| 1904 |
+
"version": "1.11.1",
|
| 1905 |
+
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
|
| 1906 |
+
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
|
| 1907 |
+
"dev": true,
|
| 1908 |
+
"license": "MIT",
|
| 1909 |
+
"optional": true,
|
| 1910 |
+
"dependencies": {
|
| 1911 |
+
"@emnapi/wasi-threads": "1.2.2",
|
| 1912 |
+
"tslib": "^2.4.0"
|
| 1913 |
+
}
|
| 1914 |
+
},
|
| 1915 |
+
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
|
| 1916 |
+
"version": "1.11.1",
|
| 1917 |
+
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
|
| 1918 |
+
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
|
| 1919 |
+
"dev": true,
|
| 1920 |
+
"license": "MIT",
|
| 1921 |
+
"optional": true,
|
| 1922 |
+
"dependencies": {
|
| 1923 |
+
"tslib": "^2.4.0"
|
| 1924 |
+
}
|
| 1925 |
+
},
|
| 1926 |
+
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
| 1927 |
+
"version": "1.2.2",
|
| 1928 |
+
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
| 1929 |
+
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
|
| 1930 |
+
"dev": true,
|
| 1931 |
+
"license": "MIT",
|
| 1932 |
+
"optional": true,
|
| 1933 |
+
"dependencies": {
|
| 1934 |
+
"tslib": "^2.4.0"
|
| 1935 |
+
}
|
| 1936 |
+
},
|
| 1937 |
+
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
| 1938 |
+
"version": "1.1.5",
|
| 1939 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
|
| 1940 |
+
"integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
|
| 1941 |
+
"cpu": [
|
| 1942 |
+
"arm64"
|
| 1943 |
+
],
|
| 1944 |
+
"dev": true,
|
| 1945 |
+
"license": "MIT",
|
| 1946 |
+
"optional": true,
|
| 1947 |
+
"os": [
|
| 1948 |
+
"win32"
|
| 1949 |
+
],
|
| 1950 |
+
"engines": {
|
| 1951 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 1952 |
+
}
|
| 1953 |
+
},
|
| 1954 |
+
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
| 1955 |
+
"version": "1.1.5",
|
| 1956 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
|
| 1957 |
+
"integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
|
| 1958 |
+
"cpu": [
|
| 1959 |
+
"x64"
|
| 1960 |
+
],
|
| 1961 |
+
"dev": true,
|
| 1962 |
+
"license": "MIT",
|
| 1963 |
+
"optional": true,
|
| 1964 |
+
"os": [
|
| 1965 |
+
"win32"
|
| 1966 |
+
],
|
| 1967 |
+
"engines": {
|
| 1968 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 1969 |
+
}
|
| 1970 |
+
},
|
| 1971 |
+
"node_modules/@rolldown/pluginutils": {
|
| 1972 |
+
"version": "1.0.1",
|
| 1973 |
+
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
| 1974 |
+
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
| 1975 |
+
"dev": true,
|
| 1976 |
+
"license": "MIT"
|
| 1977 |
+
},
|
| 1978 |
+
"node_modules/@rtsao/scc": {
|
| 1979 |
+
"version": "1.1.0",
|
| 1980 |
+
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
| 1981 |
+
"integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
|
| 1982 |
+
"dev": true,
|
| 1983 |
+
"license": "MIT"
|
| 1984 |
+
},
|
| 1985 |
+
"node_modules/@standard-schema/spec": {
|
| 1986 |
+
"version": "1.1.0",
|
| 1987 |
+
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
| 1988 |
+
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
| 1989 |
+
"license": "MIT"
|
| 1990 |
+
},
|
| 1991 |
+
"node_modules/@swc/helpers": {
|
| 1992 |
+
"version": "0.5.15",
|
| 1993 |
+
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
| 1994 |
+
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
|
| 1995 |
+
"license": "Apache-2.0",
|
| 1996 |
+
"dependencies": {
|
| 1997 |
+
"tslib": "^2.8.0"
|
| 1998 |
+
}
|
| 1999 |
+
},
|
| 2000 |
+
"node_modules/@tailwindcss/node": {
|
| 2001 |
+
"version": "4.3.2",
|
| 2002 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz",
|
| 2003 |
+
"integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==",
|
| 2004 |
+
"dev": true,
|
| 2005 |
+
"license": "MIT",
|
| 2006 |
+
"dependencies": {
|
| 2007 |
+
"@jridgewell/remapping": "^2.3.5",
|
| 2008 |
+
"enhanced-resolve": "5.21.6",
|
| 2009 |
+
"jiti": "^2.7.0",
|
| 2010 |
+
"lightningcss": "1.32.0",
|
| 2011 |
+
"magic-string": "^0.30.21",
|
| 2012 |
+
"source-map-js": "^1.2.1",
|
| 2013 |
+
"tailwindcss": "4.3.2"
|
| 2014 |
+
}
|
| 2015 |
+
},
|
| 2016 |
+
"node_modules/@tailwindcss/oxide": {
|
| 2017 |
+
"version": "4.3.2",
|
| 2018 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz",
|
| 2019 |
+
"integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==",
|
| 2020 |
+
"dev": true,
|
| 2021 |
+
"license": "MIT",
|
| 2022 |
+
"engines": {
|
| 2023 |
+
"node": ">= 20"
|
| 2024 |
+
},
|
| 2025 |
+
"optionalDependencies": {
|
| 2026 |
+
"@tailwindcss/oxide-android-arm64": "4.3.2",
|
| 2027 |
+
"@tailwindcss/oxide-darwin-arm64": "4.3.2",
|
| 2028 |
+
"@tailwindcss/oxide-darwin-x64": "4.3.2",
|
| 2029 |
+
"@tailwindcss/oxide-freebsd-x64": "4.3.2",
|
| 2030 |
+
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2",
|
| 2031 |
+
"@tailwindcss/oxide-linux-arm64-gnu": "4.3.2",
|
| 2032 |
+
"@tailwindcss/oxide-linux-arm64-musl": "4.3.2",
|
| 2033 |
+
"@tailwindcss/oxide-linux-x64-gnu": "4.3.2",
|
| 2034 |
+
"@tailwindcss/oxide-linux-x64-musl": "4.3.2",
|
| 2035 |
+
"@tailwindcss/oxide-wasm32-wasi": "4.3.2",
|
| 2036 |
+
"@tailwindcss/oxide-win32-arm64-msvc": "4.3.2",
|
| 2037 |
+
"@tailwindcss/oxide-win32-x64-msvc": "4.3.2"
|
| 2038 |
+
}
|
| 2039 |
+
},
|
| 2040 |
+
"node_modules/@tailwindcss/oxide-android-arm64": {
|
| 2041 |
+
"version": "4.3.2",
|
| 2042 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz",
|
| 2043 |
+
"integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==",
|
| 2044 |
+
"cpu": [
|
| 2045 |
+
"arm64"
|
| 2046 |
+
],
|
| 2047 |
+
"dev": true,
|
| 2048 |
+
"license": "MIT",
|
| 2049 |
+
"optional": true,
|
| 2050 |
+
"os": [
|
| 2051 |
+
"android"
|
| 2052 |
+
],
|
| 2053 |
+
"engines": {
|
| 2054 |
+
"node": ">= 20"
|
| 2055 |
+
}
|
| 2056 |
+
},
|
| 2057 |
+
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
| 2058 |
+
"version": "4.3.2",
|
| 2059 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz",
|
| 2060 |
+
"integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==",
|
| 2061 |
+
"cpu": [
|
| 2062 |
+
"arm64"
|
| 2063 |
+
],
|
| 2064 |
+
"dev": true,
|
| 2065 |
+
"license": "MIT",
|
| 2066 |
+
"optional": true,
|
| 2067 |
+
"os": [
|
| 2068 |
+
"darwin"
|
| 2069 |
+
],
|
| 2070 |
+
"engines": {
|
| 2071 |
+
"node": ">= 20"
|
| 2072 |
+
}
|
| 2073 |
+
},
|
| 2074 |
+
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
| 2075 |
+
"version": "4.3.2",
|
| 2076 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz",
|
| 2077 |
+
"integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==",
|
| 2078 |
+
"cpu": [
|
| 2079 |
+
"x64"
|
| 2080 |
+
],
|
| 2081 |
+
"dev": true,
|
| 2082 |
+
"license": "MIT",
|
| 2083 |
+
"optional": true,
|
| 2084 |
+
"os": [
|
| 2085 |
+
"darwin"
|
| 2086 |
+
],
|
| 2087 |
+
"engines": {
|
| 2088 |
+
"node": ">= 20"
|
| 2089 |
+
}
|
| 2090 |
+
},
|
| 2091 |
+
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
| 2092 |
+
"version": "4.3.2",
|
| 2093 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz",
|
| 2094 |
+
"integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==",
|
| 2095 |
+
"cpu": [
|
| 2096 |
+
"x64"
|
| 2097 |
+
],
|
| 2098 |
+
"dev": true,
|
| 2099 |
+
"license": "MIT",
|
| 2100 |
+
"optional": true,
|
| 2101 |
+
"os": [
|
| 2102 |
+
"freebsd"
|
| 2103 |
+
],
|
| 2104 |
+
"engines": {
|
| 2105 |
+
"node": ">= 20"
|
| 2106 |
+
}
|
| 2107 |
+
},
|
| 2108 |
+
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
| 2109 |
+
"version": "4.3.2",
|
| 2110 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz",
|
| 2111 |
+
"integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==",
|
| 2112 |
+
"cpu": [
|
| 2113 |
+
"arm"
|
| 2114 |
+
],
|
| 2115 |
+
"dev": true,
|
| 2116 |
+
"license": "MIT",
|
| 2117 |
+
"optional": true,
|
| 2118 |
+
"os": [
|
| 2119 |
+
"linux"
|
| 2120 |
+
],
|
| 2121 |
+
"engines": {
|
| 2122 |
+
"node": ">= 20"
|
| 2123 |
+
}
|
| 2124 |
+
},
|
| 2125 |
+
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
| 2126 |
+
"version": "4.3.2",
|
| 2127 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz",
|
| 2128 |
+
"integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==",
|
| 2129 |
+
"cpu": [
|
| 2130 |
+
"arm64"
|
| 2131 |
+
],
|
| 2132 |
+
"dev": true,
|
| 2133 |
+
"libc": [
|
| 2134 |
+
"glibc"
|
| 2135 |
+
],
|
| 2136 |
+
"license": "MIT",
|
| 2137 |
+
"optional": true,
|
| 2138 |
+
"os": [
|
| 2139 |
+
"linux"
|
| 2140 |
+
],
|
| 2141 |
+
"engines": {
|
| 2142 |
+
"node": ">= 20"
|
| 2143 |
+
}
|
| 2144 |
+
},
|
| 2145 |
+
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
| 2146 |
+
"version": "4.3.2",
|
| 2147 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz",
|
| 2148 |
+
"integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==",
|
| 2149 |
+
"cpu": [
|
| 2150 |
+
"arm64"
|
| 2151 |
+
],
|
| 2152 |
+
"dev": true,
|
| 2153 |
+
"libc": [
|
| 2154 |
+
"musl"
|
| 2155 |
+
],
|
| 2156 |
+
"license": "MIT",
|
| 2157 |
+
"optional": true,
|
| 2158 |
+
"os": [
|
| 2159 |
+
"linux"
|
| 2160 |
+
],
|
| 2161 |
+
"engines": {
|
| 2162 |
+
"node": ">= 20"
|
| 2163 |
+
}
|
| 2164 |
+
},
|
| 2165 |
+
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
| 2166 |
+
"version": "4.3.2",
|
| 2167 |
+
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz",
|
| 2168 |
+
"integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==",
|
| 2169 |
+
"cpu": [
|
| 2170 |
+
"x64"
|
| 2171 |
],
|
| 2172 |
"dev": true,
|
| 2173 |
"libc": [
|
|
|
|
| 2280 |
"tailwindcss": "4.3.2"
|
| 2281 |
}
|
| 2282 |
},
|
| 2283 |
+
"node_modules/@testing-library/dom": {
|
| 2284 |
+
"version": "10.4.1",
|
| 2285 |
+
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
| 2286 |
+
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
| 2287 |
+
"dev": true,
|
| 2288 |
+
"license": "MIT",
|
| 2289 |
+
"peer": true,
|
| 2290 |
+
"dependencies": {
|
| 2291 |
+
"@babel/code-frame": "^7.10.4",
|
| 2292 |
+
"@babel/runtime": "^7.12.5",
|
| 2293 |
+
"@types/aria-query": "^5.0.1",
|
| 2294 |
+
"aria-query": "5.3.0",
|
| 2295 |
+
"dom-accessibility-api": "^0.5.9",
|
| 2296 |
+
"lz-string": "^1.5.0",
|
| 2297 |
+
"picocolors": "1.1.1",
|
| 2298 |
+
"pretty-format": "^27.0.2"
|
| 2299 |
+
},
|
| 2300 |
+
"engines": {
|
| 2301 |
+
"node": ">=18"
|
| 2302 |
+
}
|
| 2303 |
+
},
|
| 2304 |
+
"node_modules/@testing-library/dom/node_modules/aria-query": {
|
| 2305 |
+
"version": "5.3.0",
|
| 2306 |
+
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
|
| 2307 |
+
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
|
| 2308 |
+
"dev": true,
|
| 2309 |
+
"license": "Apache-2.0",
|
| 2310 |
+
"peer": true,
|
| 2311 |
+
"dependencies": {
|
| 2312 |
+
"dequal": "^2.0.3"
|
| 2313 |
+
}
|
| 2314 |
+
},
|
| 2315 |
+
"node_modules/@testing-library/jest-dom": {
|
| 2316 |
+
"version": "6.9.1",
|
| 2317 |
+
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
|
| 2318 |
+
"integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
|
| 2319 |
+
"dev": true,
|
| 2320 |
+
"license": "MIT",
|
| 2321 |
+
"dependencies": {
|
| 2322 |
+
"@adobe/css-tools": "^4.4.0",
|
| 2323 |
+
"aria-query": "^5.0.0",
|
| 2324 |
+
"css.escape": "^1.5.1",
|
| 2325 |
+
"dom-accessibility-api": "^0.6.3",
|
| 2326 |
+
"picocolors": "^1.1.1",
|
| 2327 |
+
"redent": "^3.0.0"
|
| 2328 |
+
},
|
| 2329 |
+
"engines": {
|
| 2330 |
+
"node": ">=14",
|
| 2331 |
+
"npm": ">=6",
|
| 2332 |
+
"yarn": ">=1"
|
| 2333 |
+
}
|
| 2334 |
+
},
|
| 2335 |
+
"node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
|
| 2336 |
+
"version": "0.6.3",
|
| 2337 |
+
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
|
| 2338 |
+
"integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
|
| 2339 |
+
"dev": true,
|
| 2340 |
+
"license": "MIT"
|
| 2341 |
+
},
|
| 2342 |
+
"node_modules/@testing-library/react": {
|
| 2343 |
+
"version": "16.3.2",
|
| 2344 |
+
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz",
|
| 2345 |
+
"integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
|
| 2346 |
+
"dev": true,
|
| 2347 |
+
"license": "MIT",
|
| 2348 |
+
"dependencies": {
|
| 2349 |
+
"@babel/runtime": "^7.12.5"
|
| 2350 |
+
},
|
| 2351 |
+
"engines": {
|
| 2352 |
+
"node": ">=18"
|
| 2353 |
+
},
|
| 2354 |
+
"peerDependencies": {
|
| 2355 |
+
"@testing-library/dom": "^10.0.0",
|
| 2356 |
+
"@types/react": "^18.0.0 || ^19.0.0",
|
| 2357 |
+
"@types/react-dom": "^18.0.0 || ^19.0.0",
|
| 2358 |
+
"react": "^18.0.0 || ^19.0.0",
|
| 2359 |
+
"react-dom": "^18.0.0 || ^19.0.0"
|
| 2360 |
+
},
|
| 2361 |
+
"peerDependenciesMeta": {
|
| 2362 |
+
"@types/react": {
|
| 2363 |
+
"optional": true
|
| 2364 |
+
},
|
| 2365 |
+
"@types/react-dom": {
|
| 2366 |
+
"optional": true
|
| 2367 |
+
}
|
| 2368 |
+
}
|
| 2369 |
+
},
|
| 2370 |
+
"node_modules/@testing-library/user-event": {
|
| 2371 |
+
"version": "14.6.1",
|
| 2372 |
+
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
|
| 2373 |
+
"integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
|
| 2374 |
+
"dev": true,
|
| 2375 |
+
"license": "MIT",
|
| 2376 |
+
"engines": {
|
| 2377 |
+
"node": ">=12",
|
| 2378 |
+
"npm": ">=6"
|
| 2379 |
+
},
|
| 2380 |
+
"peerDependencies": {
|
| 2381 |
+
"@testing-library/dom": ">=7.21.4"
|
| 2382 |
+
}
|
| 2383 |
+
},
|
| 2384 |
"node_modules/@tybys/wasm-util": {
|
| 2385 |
"version": "0.10.3",
|
| 2386 |
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
|
|
|
|
| 2392 |
"tslib": "^2.4.0"
|
| 2393 |
}
|
| 2394 |
},
|
| 2395 |
+
"node_modules/@types/aria-query": {
|
| 2396 |
+
"version": "5.0.4",
|
| 2397 |
+
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
| 2398 |
+
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
| 2399 |
+
"dev": true,
|
| 2400 |
+
"license": "MIT",
|
| 2401 |
+
"peer": true
|
| 2402 |
+
},
|
| 2403 |
+
"node_modules/@types/chai": {
|
| 2404 |
+
"version": "5.2.3",
|
| 2405 |
+
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
| 2406 |
+
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
| 2407 |
+
"dev": true,
|
| 2408 |
+
"license": "MIT",
|
| 2409 |
+
"dependencies": {
|
| 2410 |
+
"@types/deep-eql": "*",
|
| 2411 |
+
"assertion-error": "^2.0.1"
|
| 2412 |
+
}
|
| 2413 |
+
},
|
| 2414 |
"node_modules/@types/debug": {
|
| 2415 |
"version": "4.1.13",
|
| 2416 |
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
|
|
|
|
| 2420 |
"@types/ms": "*"
|
| 2421 |
}
|
| 2422 |
},
|
| 2423 |
+
"node_modules/@types/deep-eql": {
|
| 2424 |
+
"version": "4.0.2",
|
| 2425 |
+
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
| 2426 |
+
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
| 2427 |
+
"dev": true,
|
| 2428 |
+
"license": "MIT"
|
| 2429 |
+
},
|
| 2430 |
"node_modules/@types/estree": {
|
| 2431 |
"version": "1.0.9",
|
| 2432 |
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
|
|
|
| 3142 |
"win32"
|
| 3143 |
]
|
| 3144 |
},
|
| 3145 |
+
"node_modules/@unrs/resolver-binding-win32-ia32-msvc": {
|
| 3146 |
+
"version": "1.12.2",
|
| 3147 |
+
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz",
|
| 3148 |
+
"integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==",
|
| 3149 |
+
"cpu": [
|
| 3150 |
+
"ia32"
|
| 3151 |
+
],
|
| 3152 |
+
"dev": true,
|
| 3153 |
+
"license": "MIT",
|
| 3154 |
+
"optional": true,
|
| 3155 |
+
"os": [
|
| 3156 |
+
"win32"
|
| 3157 |
+
]
|
| 3158 |
+
},
|
| 3159 |
+
"node_modules/@unrs/resolver-binding-win32-x64-msvc": {
|
| 3160 |
+
"version": "1.12.2",
|
| 3161 |
+
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz",
|
| 3162 |
+
"integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==",
|
| 3163 |
+
"cpu": [
|
| 3164 |
+
"x64"
|
| 3165 |
+
],
|
| 3166 |
+
"dev": true,
|
| 3167 |
+
"license": "MIT",
|
| 3168 |
+
"optional": true,
|
| 3169 |
+
"os": [
|
| 3170 |
+
"win32"
|
| 3171 |
+
]
|
| 3172 |
+
},
|
| 3173 |
+
"node_modules/@vercel/oidc": {
|
| 3174 |
+
"version": "3.2.0",
|
| 3175 |
+
"resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.2.0.tgz",
|
| 3176 |
+
"integrity": "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==",
|
| 3177 |
+
"license": "Apache-2.0",
|
| 3178 |
+
"engines": {
|
| 3179 |
+
"node": ">= 20"
|
| 3180 |
+
}
|
| 3181 |
+
},
|
| 3182 |
+
"node_modules/@vitest/expect": {
|
| 3183 |
+
"version": "4.1.10",
|
| 3184 |
+
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",
|
| 3185 |
+
"integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==",
|
| 3186 |
+
"dev": true,
|
| 3187 |
+
"license": "MIT",
|
| 3188 |
+
"dependencies": {
|
| 3189 |
+
"@standard-schema/spec": "^1.1.0",
|
| 3190 |
+
"@types/chai": "^5.2.2",
|
| 3191 |
+
"@vitest/spy": "4.1.10",
|
| 3192 |
+
"@vitest/utils": "4.1.10",
|
| 3193 |
+
"chai": "^6.2.2",
|
| 3194 |
+
"tinyrainbow": "^3.1.0"
|
| 3195 |
+
},
|
| 3196 |
+
"funding": {
|
| 3197 |
+
"url": "https://opencollective.com/vitest"
|
| 3198 |
+
}
|
| 3199 |
+
},
|
| 3200 |
+
"node_modules/@vitest/mocker": {
|
| 3201 |
+
"version": "4.1.10",
|
| 3202 |
+
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz",
|
| 3203 |
+
"integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==",
|
| 3204 |
+
"dev": true,
|
| 3205 |
+
"license": "MIT",
|
| 3206 |
+
"dependencies": {
|
| 3207 |
+
"@vitest/spy": "4.1.10",
|
| 3208 |
+
"estree-walker": "^3.0.3",
|
| 3209 |
+
"magic-string": "^0.30.21"
|
| 3210 |
+
},
|
| 3211 |
+
"funding": {
|
| 3212 |
+
"url": "https://opencollective.com/vitest"
|
| 3213 |
+
},
|
| 3214 |
+
"peerDependencies": {
|
| 3215 |
+
"msw": "^2.4.9",
|
| 3216 |
+
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
| 3217 |
+
},
|
| 3218 |
+
"peerDependenciesMeta": {
|
| 3219 |
+
"msw": {
|
| 3220 |
+
"optional": true
|
| 3221 |
+
},
|
| 3222 |
+
"vite": {
|
| 3223 |
+
"optional": true
|
| 3224 |
+
}
|
| 3225 |
+
}
|
| 3226 |
+
},
|
| 3227 |
+
"node_modules/@vitest/pretty-format": {
|
| 3228 |
+
"version": "4.1.10",
|
| 3229 |
+
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz",
|
| 3230 |
+
"integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==",
|
| 3231 |
+
"dev": true,
|
| 3232 |
+
"license": "MIT",
|
| 3233 |
+
"dependencies": {
|
| 3234 |
+
"tinyrainbow": "^3.1.0"
|
| 3235 |
+
},
|
| 3236 |
+
"funding": {
|
| 3237 |
+
"url": "https://opencollective.com/vitest"
|
| 3238 |
+
}
|
| 3239 |
+
},
|
| 3240 |
+
"node_modules/@vitest/runner": {
|
| 3241 |
+
"version": "4.1.10",
|
| 3242 |
+
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz",
|
| 3243 |
+
"integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==",
|
| 3244 |
+
"dev": true,
|
| 3245 |
+
"license": "MIT",
|
| 3246 |
+
"dependencies": {
|
| 3247 |
+
"@vitest/utils": "4.1.10",
|
| 3248 |
+
"pathe": "^2.0.3"
|
| 3249 |
+
},
|
| 3250 |
+
"funding": {
|
| 3251 |
+
"url": "https://opencollective.com/vitest"
|
| 3252 |
+
}
|
| 3253 |
+
},
|
| 3254 |
+
"node_modules/@vitest/snapshot": {
|
| 3255 |
+
"version": "4.1.10",
|
| 3256 |
+
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz",
|
| 3257 |
+
"integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==",
|
| 3258 |
"dev": true,
|
| 3259 |
"license": "MIT",
|
| 3260 |
+
"dependencies": {
|
| 3261 |
+
"@vitest/pretty-format": "4.1.10",
|
| 3262 |
+
"@vitest/utils": "4.1.10",
|
| 3263 |
+
"magic-string": "^0.30.21",
|
| 3264 |
+
"pathe": "^2.0.3"
|
| 3265 |
+
},
|
| 3266 |
+
"funding": {
|
| 3267 |
+
"url": "https://opencollective.com/vitest"
|
| 3268 |
+
}
|
| 3269 |
},
|
| 3270 |
+
"node_modules/@vitest/spy": {
|
| 3271 |
+
"version": "4.1.10",
|
| 3272 |
+
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz",
|
| 3273 |
+
"integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==",
|
|
|
|
|
|
|
|
|
|
| 3274 |
"dev": true,
|
| 3275 |
"license": "MIT",
|
| 3276 |
+
"funding": {
|
| 3277 |
+
"url": "https://opencollective.com/vitest"
|
| 3278 |
+
}
|
|
|
|
| 3279 |
},
|
| 3280 |
+
"node_modules/@vitest/utils": {
|
| 3281 |
+
"version": "4.1.10",
|
| 3282 |
+
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz",
|
| 3283 |
+
"integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==",
|
| 3284 |
+
"dev": true,
|
| 3285 |
+
"license": "MIT",
|
| 3286 |
+
"dependencies": {
|
| 3287 |
+
"@vitest/pretty-format": "4.1.10",
|
| 3288 |
+
"convert-source-map": "^2.0.0",
|
| 3289 |
+
"tinyrainbow": "^3.1.0"
|
| 3290 |
+
},
|
| 3291 |
+
"funding": {
|
| 3292 |
+
"url": "https://opencollective.com/vitest"
|
| 3293 |
}
|
| 3294 |
},
|
| 3295 |
"node_modules/acorn": {
|
|
|
|
| 3350 |
"url": "https://github.com/sponsors/epoberezkin"
|
| 3351 |
}
|
| 3352 |
},
|
| 3353 |
+
"node_modules/ansi-regex": {
|
| 3354 |
+
"version": "5.0.1",
|
| 3355 |
+
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
| 3356 |
+
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
| 3357 |
+
"dev": true,
|
| 3358 |
+
"license": "MIT",
|
| 3359 |
+
"peer": true,
|
| 3360 |
+
"engines": {
|
| 3361 |
+
"node": ">=8"
|
| 3362 |
+
}
|
| 3363 |
+
},
|
| 3364 |
"node_modules/ansi-styles": {
|
| 3365 |
"version": "4.3.0",
|
| 3366 |
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
|
|
|
| 3554 |
"url": "https://github.com/sponsors/ljharb"
|
| 3555 |
}
|
| 3556 |
},
|
| 3557 |
+
"node_modules/assertion-error": {
|
| 3558 |
+
"version": "2.0.1",
|
| 3559 |
+
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
| 3560 |
+
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
| 3561 |
+
"dev": true,
|
| 3562 |
+
"license": "MIT",
|
| 3563 |
+
"engines": {
|
| 3564 |
+
"node": ">=12"
|
| 3565 |
+
}
|
| 3566 |
+
},
|
| 3567 |
"node_modules/ast-types-flow": {
|
| 3568 |
"version": "0.0.8",
|
| 3569 |
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
|
|
|
|
| 3646 |
"node": ">=6.0.0"
|
| 3647 |
}
|
| 3648 |
},
|
| 3649 |
+
"node_modules/bidi-js": {
|
| 3650 |
+
"version": "1.0.3",
|
| 3651 |
+
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
| 3652 |
+
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
|
| 3653 |
+
"dev": true,
|
| 3654 |
+
"license": "MIT",
|
| 3655 |
+
"dependencies": {
|
| 3656 |
+
"require-from-string": "^2.0.2"
|
| 3657 |
+
}
|
| 3658 |
+
},
|
| 3659 |
"node_modules/brace-expansion": {
|
| 3660 |
"version": "1.1.15",
|
| 3661 |
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
|
|
|
|
| 3804 |
"url": "https://github.com/sponsors/wooorm"
|
| 3805 |
}
|
| 3806 |
},
|
| 3807 |
+
"node_modules/chai": {
|
| 3808 |
+
"version": "6.2.2",
|
| 3809 |
+
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
| 3810 |
+
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
| 3811 |
+
"dev": true,
|
| 3812 |
+
"license": "MIT",
|
| 3813 |
+
"engines": {
|
| 3814 |
+
"node": ">=18"
|
| 3815 |
+
}
|
| 3816 |
+
},
|
| 3817 |
"node_modules/chalk": {
|
| 3818 |
"version": "4.1.2",
|
| 3819 |
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
|
|
|
| 3945 |
"node": ">= 8"
|
| 3946 |
}
|
| 3947 |
},
|
| 3948 |
+
"node_modules/css-tree": {
|
| 3949 |
+
"version": "3.2.1",
|
| 3950 |
+
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
|
| 3951 |
+
"integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
|
| 3952 |
+
"dev": true,
|
| 3953 |
+
"license": "MIT",
|
| 3954 |
+
"dependencies": {
|
| 3955 |
+
"mdn-data": "2.27.1",
|
| 3956 |
+
"source-map-js": "^1.2.1"
|
| 3957 |
+
},
|
| 3958 |
+
"engines": {
|
| 3959 |
+
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
| 3960 |
+
}
|
| 3961 |
+
},
|
| 3962 |
+
"node_modules/css.escape": {
|
| 3963 |
+
"version": "1.5.1",
|
| 3964 |
+
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
|
| 3965 |
+
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
|
| 3966 |
+
"dev": true,
|
| 3967 |
+
"license": "MIT"
|
| 3968 |
+
},
|
| 3969 |
"node_modules/csstype": {
|
| 3970 |
"version": "3.2.3",
|
| 3971 |
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
|
|
|
| 3979 |
"dev": true,
|
| 3980 |
"license": "BSD-2-Clause"
|
| 3981 |
},
|
| 3982 |
+
"node_modules/data-urls": {
|
| 3983 |
+
"version": "7.0.0",
|
| 3984 |
+
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
|
| 3985 |
+
"integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
|
| 3986 |
+
"dev": true,
|
| 3987 |
+
"license": "MIT",
|
| 3988 |
+
"dependencies": {
|
| 3989 |
+
"whatwg-mimetype": "^5.0.0",
|
| 3990 |
+
"whatwg-url": "^16.0.0"
|
| 3991 |
+
},
|
| 3992 |
+
"engines": {
|
| 3993 |
+
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
| 3994 |
+
}
|
| 3995 |
+
},
|
| 3996 |
"node_modules/data-view-buffer": {
|
| 3997 |
"version": "1.0.2",
|
| 3998 |
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
|
|
|
|
| 4064 |
}
|
| 4065 |
}
|
| 4066 |
},
|
| 4067 |
+
"node_modules/decimal.js": {
|
| 4068 |
+
"version": "10.6.0",
|
| 4069 |
+
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
| 4070 |
+
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
| 4071 |
+
"dev": true,
|
| 4072 |
+
"license": "MIT"
|
| 4073 |
+
},
|
| 4074 |
"node_modules/decode-named-character-reference": {
|
| 4075 |
"version": "1.3.0",
|
| 4076 |
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
|
|
|
|
| 4172 |
"node": ">=0.10.0"
|
| 4173 |
}
|
| 4174 |
},
|
| 4175 |
+
"node_modules/dom-accessibility-api": {
|
| 4176 |
+
"version": "0.5.16",
|
| 4177 |
+
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
| 4178 |
+
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
| 4179 |
+
"dev": true,
|
| 4180 |
+
"license": "MIT",
|
| 4181 |
+
"peer": true
|
| 4182 |
+
},
|
| 4183 |
"node_modules/dunder-proto": {
|
| 4184 |
"version": "1.0.1",
|
| 4185 |
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
|
|
|
| 4223 |
"node": ">=10.13.0"
|
| 4224 |
}
|
| 4225 |
},
|
| 4226 |
+
"node_modules/entities": {
|
| 4227 |
+
"version": "8.0.0",
|
| 4228 |
+
"resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
|
| 4229 |
+
"integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
|
| 4230 |
+
"dev": true,
|
| 4231 |
+
"license": "BSD-2-Clause",
|
| 4232 |
+
"engines": {
|
| 4233 |
+
"node": ">=20.19.0"
|
| 4234 |
+
},
|
| 4235 |
+
"funding": {
|
| 4236 |
+
"url": "https://github.com/fb55/entities?sponsor=1"
|
| 4237 |
+
}
|
| 4238 |
+
},
|
| 4239 |
"node_modules/es-abstract": {
|
| 4240 |
"version": "1.24.2",
|
| 4241 |
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz",
|
|
|
|
| 4372 |
"node": ">= 0.4"
|
| 4373 |
}
|
| 4374 |
},
|
| 4375 |
+
"node_modules/es-module-lexer": {
|
| 4376 |
+
"version": "2.3.1",
|
| 4377 |
+
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz",
|
| 4378 |
+
"integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==",
|
| 4379 |
+
"dev": true,
|
| 4380 |
+
"license": "MIT"
|
| 4381 |
+
},
|
| 4382 |
"node_modules/es-object-atoms": {
|
| 4383 |
"version": "1.1.2",
|
| 4384 |
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
|
|
|
| 4871 |
"url": "https://opencollective.com/unified"
|
| 4872 |
}
|
| 4873 |
},
|
| 4874 |
+
"node_modules/estree-walker": {
|
| 4875 |
+
"version": "3.0.3",
|
| 4876 |
+
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
| 4877 |
+
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
| 4878 |
+
"dev": true,
|
| 4879 |
+
"license": "MIT",
|
| 4880 |
+
"dependencies": {
|
| 4881 |
+
"@types/estree": "^1.0.0"
|
| 4882 |
+
}
|
| 4883 |
+
},
|
| 4884 |
"node_modules/esutils": {
|
| 4885 |
"version": "2.0.3",
|
| 4886 |
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
|
|
|
| 4900 |
"node": ">=18.0.0"
|
| 4901 |
}
|
| 4902 |
},
|
| 4903 |
+
"node_modules/expect-type": {
|
| 4904 |
+
"version": "1.4.0",
|
| 4905 |
+
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
|
| 4906 |
+
"integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
|
| 4907 |
+
"dev": true,
|
| 4908 |
+
"license": "Apache-2.0",
|
| 4909 |
+
"engines": {
|
| 4910 |
+
"node": ">=12.0.0"
|
| 4911 |
+
}
|
| 4912 |
+
},
|
| 4913 |
"node_modules/extend": {
|
| 4914 |
"version": "3.0.2",
|
| 4915 |
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
|
|
|
| 5057 |
"url": "https://github.com/sponsors/ljharb"
|
| 5058 |
}
|
| 5059 |
},
|
| 5060 |
+
"node_modules/fsevents": {
|
| 5061 |
+
"version": "2.3.3",
|
| 5062 |
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
| 5063 |
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
| 5064 |
+
"dev": true,
|
| 5065 |
+
"hasInstallScript": true,
|
| 5066 |
+
"license": "MIT",
|
| 5067 |
+
"optional": true,
|
| 5068 |
+
"os": [
|
| 5069 |
+
"darwin"
|
| 5070 |
+
],
|
| 5071 |
+
"engines": {
|
| 5072 |
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 5073 |
+
}
|
| 5074 |
+
},
|
| 5075 |
"node_modules/function-bind": {
|
| 5076 |
"version": "1.1.2",
|
| 5077 |
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
|
|
|
| 5458 |
"node": ">=12.0.0"
|
| 5459 |
}
|
| 5460 |
},
|
| 5461 |
+
"node_modules/html-encoding-sniffer": {
|
| 5462 |
+
"version": "6.0.0",
|
| 5463 |
+
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
|
| 5464 |
+
"integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
|
| 5465 |
+
"dev": true,
|
| 5466 |
+
"license": "MIT",
|
| 5467 |
+
"dependencies": {
|
| 5468 |
+
"@exodus/bytes": "^1.6.0"
|
| 5469 |
+
},
|
| 5470 |
+
"engines": {
|
| 5471 |
+
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
| 5472 |
+
}
|
| 5473 |
+
},
|
| 5474 |
"node_modules/html-url-attributes": {
|
| 5475 |
"version": "3.0.1",
|
| 5476 |
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
|
|
|
|
| 5518 |
"node": ">=0.8.19"
|
| 5519 |
}
|
| 5520 |
},
|
| 5521 |
+
"node_modules/indent-string": {
|
| 5522 |
+
"version": "4.0.0",
|
| 5523 |
+
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
| 5524 |
+
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
| 5525 |
+
"dev": true,
|
| 5526 |
+
"license": "MIT",
|
| 5527 |
+
"engines": {
|
| 5528 |
+
"node": ">=8"
|
| 5529 |
+
}
|
| 5530 |
+
},
|
| 5531 |
"node_modules/inline-style-parser": {
|
| 5532 |
"version": "0.2.7",
|
| 5533 |
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
|
|
|
|
| 5891 |
"url": "https://github.com/sponsors/sindresorhus"
|
| 5892 |
}
|
| 5893 |
},
|
| 5894 |
+
"node_modules/is-potential-custom-element-name": {
|
| 5895 |
+
"version": "1.0.1",
|
| 5896 |
+
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
| 5897 |
+
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
| 5898 |
+
"dev": true,
|
| 5899 |
+
"license": "MIT"
|
| 5900 |
+
},
|
| 5901 |
"node_modules/is-regex": {
|
| 5902 |
"version": "1.2.1",
|
| 5903 |
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
|
|
|
| 6115 |
"js-yaml": "bin/js-yaml.js"
|
| 6116 |
}
|
| 6117 |
},
|
| 6118 |
+
"node_modules/jsdom": {
|
| 6119 |
+
"version": "29.1.1",
|
| 6120 |
+
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
|
| 6121 |
+
"integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==",
|
| 6122 |
+
"dev": true,
|
| 6123 |
+
"license": "MIT",
|
| 6124 |
+
"dependencies": {
|
| 6125 |
+
"@asamuzakjp/css-color": "^5.1.11",
|
| 6126 |
+
"@asamuzakjp/dom-selector": "^7.1.1",
|
| 6127 |
+
"@bramus/specificity": "^2.4.2",
|
| 6128 |
+
"@csstools/css-syntax-patches-for-csstree": "^1.1.3",
|
| 6129 |
+
"@exodus/bytes": "^1.15.0",
|
| 6130 |
+
"css-tree": "^3.2.1",
|
| 6131 |
+
"data-urls": "^7.0.0",
|
| 6132 |
+
"decimal.js": "^10.6.0",
|
| 6133 |
+
"html-encoding-sniffer": "^6.0.0",
|
| 6134 |
+
"is-potential-custom-element-name": "^1.0.1",
|
| 6135 |
+
"lru-cache": "^11.3.5",
|
| 6136 |
+
"parse5": "^8.0.1",
|
| 6137 |
+
"saxes": "^6.0.0",
|
| 6138 |
+
"symbol-tree": "^3.2.4",
|
| 6139 |
+
"tough-cookie": "^6.0.1",
|
| 6140 |
+
"undici": "^7.25.0",
|
| 6141 |
+
"w3c-xmlserializer": "^5.0.0",
|
| 6142 |
+
"webidl-conversions": "^8.0.1",
|
| 6143 |
+
"whatwg-mimetype": "^5.0.0",
|
| 6144 |
+
"whatwg-url": "^16.0.1",
|
| 6145 |
+
"xml-name-validator": "^5.0.0"
|
| 6146 |
+
},
|
| 6147 |
+
"engines": {
|
| 6148 |
+
"node": "^20.19.0 || ^22.13.0 || >=24.0.0"
|
| 6149 |
+
},
|
| 6150 |
+
"peerDependencies": {
|
| 6151 |
+
"canvas": "^3.0.0"
|
| 6152 |
+
},
|
| 6153 |
+
"peerDependenciesMeta": {
|
| 6154 |
+
"canvas": {
|
| 6155 |
+
"optional": true
|
| 6156 |
+
}
|
| 6157 |
+
}
|
| 6158 |
+
},
|
| 6159 |
+
"node_modules/jsdom/node_modules/lru-cache": {
|
| 6160 |
+
"version": "11.5.2",
|
| 6161 |
+
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
|
| 6162 |
+
"integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
|
| 6163 |
+
"dev": true,
|
| 6164 |
+
"license": "BlueOak-1.0.0",
|
| 6165 |
+
"engines": {
|
| 6166 |
+
"node": "20 || >=22"
|
| 6167 |
+
}
|
| 6168 |
+
},
|
| 6169 |
"node_modules/jsesc": {
|
| 6170 |
"version": "3.1.0",
|
| 6171 |
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
|
|
|
| 6632 |
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
| 6633 |
}
|
| 6634 |
},
|
| 6635 |
+
"node_modules/lz-string": {
|
| 6636 |
+
"version": "1.5.0",
|
| 6637 |
+
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
| 6638 |
+
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
| 6639 |
+
"dev": true,
|
| 6640 |
+
"license": "MIT",
|
| 6641 |
+
"peer": true,
|
| 6642 |
+
"bin": {
|
| 6643 |
+
"lz-string": "bin/bin.js"
|
| 6644 |
+
}
|
| 6645 |
+
},
|
| 6646 |
"node_modules/magic-string": {
|
| 6647 |
"version": "0.30.21",
|
| 6648 |
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
|
|
|
| 6955 |
"url": "https://opencollective.com/unified"
|
| 6956 |
}
|
| 6957 |
},
|
| 6958 |
+
"node_modules/mdn-data": {
|
| 6959 |
+
"version": "2.27.1",
|
| 6960 |
+
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
|
| 6961 |
+
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
|
| 6962 |
+
"dev": true,
|
| 6963 |
+
"license": "CC0-1.0"
|
| 6964 |
+
},
|
| 6965 |
"node_modules/merge2": {
|
| 6966 |
"version": "1.4.1",
|
| 6967 |
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
|
|
|
| 7549 |
"node": ">=8.6"
|
| 7550 |
}
|
| 7551 |
},
|
| 7552 |
+
"node_modules/min-indent": {
|
| 7553 |
+
"version": "1.0.1",
|
| 7554 |
+
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
| 7555 |
+
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
|
| 7556 |
+
"dev": true,
|
| 7557 |
+
"license": "MIT",
|
| 7558 |
+
"engines": {
|
| 7559 |
+
"node": ">=4"
|
| 7560 |
+
}
|
| 7561 |
+
},
|
| 7562 |
"node_modules/minimatch": {
|
| 7563 |
"version": "3.1.5",
|
| 7564 |
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
|
|
|
| 7862 |
"url": "https://github.com/sponsors/ljharb"
|
| 7863 |
}
|
| 7864 |
},
|
| 7865 |
+
"node_modules/obug": {
|
| 7866 |
+
"version": "2.1.4",
|
| 7867 |
+
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
|
| 7868 |
+
"integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==",
|
| 7869 |
+
"dev": true,
|
| 7870 |
+
"funding": [
|
| 7871 |
+
"https://github.com/sponsors/sxzz",
|
| 7872 |
+
"https://opencollective.com/debug"
|
| 7873 |
+
],
|
| 7874 |
+
"license": "MIT",
|
| 7875 |
+
"engines": {
|
| 7876 |
+
"node": ">=12.20.0"
|
| 7877 |
+
}
|
| 7878 |
+
},
|
| 7879 |
"node_modules/optionator": {
|
| 7880 |
"version": "0.9.4",
|
| 7881 |
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
|
|
|
| 7982 |
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
|
| 7983 |
"license": "MIT"
|
| 7984 |
},
|
| 7985 |
+
"node_modules/parse5": {
|
| 7986 |
+
"version": "8.0.1",
|
| 7987 |
+
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
|
| 7988 |
+
"integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
|
| 7989 |
+
"dev": true,
|
| 7990 |
+
"license": "MIT",
|
| 7991 |
+
"dependencies": {
|
| 7992 |
+
"entities": "^8.0.0"
|
| 7993 |
+
},
|
| 7994 |
+
"funding": {
|
| 7995 |
+
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
| 7996 |
+
}
|
| 7997 |
+
},
|
| 7998 |
"node_modules/path-exists": {
|
| 7999 |
"version": "4.0.0",
|
| 8000 |
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
|
|
|
| 8022 |
"dev": true,
|
| 8023 |
"license": "MIT"
|
| 8024 |
},
|
| 8025 |
+
"node_modules/pathe": {
|
| 8026 |
+
"version": "2.0.3",
|
| 8027 |
+
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
| 8028 |
+
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
| 8029 |
+
"dev": true,
|
| 8030 |
+
"license": "MIT"
|
| 8031 |
+
},
|
| 8032 |
"node_modules/picocolors": {
|
| 8033 |
"version": "1.1.1",
|
| 8034 |
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
|
|
|
| 8048 |
"url": "https://github.com/sponsors/jonschlinkert"
|
| 8049 |
}
|
| 8050 |
},
|
| 8051 |
+
"node_modules/playwright": {
|
| 8052 |
+
"version": "1.61.1",
|
| 8053 |
+
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
| 8054 |
+
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
| 8055 |
+
"devOptional": true,
|
| 8056 |
+
"license": "Apache-2.0",
|
| 8057 |
+
"dependencies": {
|
| 8058 |
+
"playwright-core": "1.61.1"
|
| 8059 |
+
},
|
| 8060 |
+
"bin": {
|
| 8061 |
+
"playwright": "cli.js"
|
| 8062 |
+
},
|
| 8063 |
+
"engines": {
|
| 8064 |
+
"node": ">=18"
|
| 8065 |
+
},
|
| 8066 |
+
"optionalDependencies": {
|
| 8067 |
+
"fsevents": "2.3.2"
|
| 8068 |
+
}
|
| 8069 |
+
},
|
| 8070 |
+
"node_modules/playwright-core": {
|
| 8071 |
+
"version": "1.61.1",
|
| 8072 |
+
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
| 8073 |
+
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
| 8074 |
+
"devOptional": true,
|
| 8075 |
+
"license": "Apache-2.0",
|
| 8076 |
+
"bin": {
|
| 8077 |
+
"playwright-core": "cli.js"
|
| 8078 |
+
},
|
| 8079 |
+
"engines": {
|
| 8080 |
+
"node": ">=18"
|
| 8081 |
+
}
|
| 8082 |
+
},
|
| 8083 |
+
"node_modules/playwright/node_modules/fsevents": {
|
| 8084 |
+
"version": "2.3.2",
|
| 8085 |
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
| 8086 |
+
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
| 8087 |
+
"hasInstallScript": true,
|
| 8088 |
+
"license": "MIT",
|
| 8089 |
+
"optional": true,
|
| 8090 |
+
"os": [
|
| 8091 |
+
"darwin"
|
| 8092 |
+
],
|
| 8093 |
+
"engines": {
|
| 8094 |
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 8095 |
+
}
|
| 8096 |
+
},
|
| 8097 |
"node_modules/possible-typed-array-names": {
|
| 8098 |
"version": "1.1.0",
|
| 8099 |
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
|
|
|
| 8105 |
}
|
| 8106 |
},
|
| 8107 |
"node_modules/postcss": {
|
| 8108 |
+
"version": "8.5.19",
|
| 8109 |
+
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz",
|
| 8110 |
+
"integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==",
|
| 8111 |
"dev": true,
|
| 8112 |
"funding": [
|
| 8113 |
{
|
|
|
|
| 8143 |
"node": ">= 0.8.0"
|
| 8144 |
}
|
| 8145 |
},
|
| 8146 |
+
"node_modules/pretty-format": {
|
| 8147 |
+
"version": "27.5.1",
|
| 8148 |
+
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
| 8149 |
+
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
| 8150 |
+
"dev": true,
|
| 8151 |
+
"license": "MIT",
|
| 8152 |
+
"peer": true,
|
| 8153 |
+
"dependencies": {
|
| 8154 |
+
"ansi-regex": "^5.0.1",
|
| 8155 |
+
"ansi-styles": "^5.0.0",
|
| 8156 |
+
"react-is": "^17.0.1"
|
| 8157 |
+
},
|
| 8158 |
+
"engines": {
|
| 8159 |
+
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
| 8160 |
+
}
|
| 8161 |
+
},
|
| 8162 |
+
"node_modules/pretty-format/node_modules/ansi-styles": {
|
| 8163 |
+
"version": "5.2.0",
|
| 8164 |
+
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
| 8165 |
+
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
| 8166 |
+
"dev": true,
|
| 8167 |
+
"license": "MIT",
|
| 8168 |
+
"peer": true,
|
| 8169 |
+
"engines": {
|
| 8170 |
+
"node": ">=10"
|
| 8171 |
+
},
|
| 8172 |
+
"funding": {
|
| 8173 |
+
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
| 8174 |
+
}
|
| 8175 |
+
},
|
| 8176 |
+
"node_modules/pretty-format/node_modules/react-is": {
|
| 8177 |
+
"version": "17.0.2",
|
| 8178 |
+
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
| 8179 |
+
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
| 8180 |
+
"dev": true,
|
| 8181 |
+
"license": "MIT",
|
| 8182 |
+
"peer": true
|
| 8183 |
+
},
|
| 8184 |
"node_modules/prop-types": {
|
| 8185 |
"version": "15.8.1",
|
| 8186 |
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
|
|
|
| 8289 |
"react": ">=18"
|
| 8290 |
}
|
| 8291 |
},
|
| 8292 |
+
"node_modules/redent": {
|
| 8293 |
+
"version": "3.0.0",
|
| 8294 |
+
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
| 8295 |
+
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
|
| 8296 |
+
"dev": true,
|
| 8297 |
+
"license": "MIT",
|
| 8298 |
+
"dependencies": {
|
| 8299 |
+
"indent-string": "^4.0.0",
|
| 8300 |
+
"strip-indent": "^3.0.0"
|
| 8301 |
+
},
|
| 8302 |
+
"engines": {
|
| 8303 |
+
"node": ">=8"
|
| 8304 |
+
}
|
| 8305 |
+
},
|
| 8306 |
"node_modules/reflect.getprototypeof": {
|
| 8307 |
"version": "1.0.10",
|
| 8308 |
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
|
|
|
| 8430 |
"url": "https://opencollective.com/unified"
|
| 8431 |
}
|
| 8432 |
},
|
| 8433 |
+
"node_modules/require-from-string": {
|
| 8434 |
+
"version": "2.0.2",
|
| 8435 |
+
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
| 8436 |
+
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
| 8437 |
+
"dev": true,
|
| 8438 |
+
"license": "MIT",
|
| 8439 |
+
"engines": {
|
| 8440 |
+
"node": ">=0.10.0"
|
| 8441 |
+
}
|
| 8442 |
+
},
|
| 8443 |
"node_modules/resolve": {
|
| 8444 |
"version": "2.0.0-next.7",
|
| 8445 |
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz",
|
|
|
|
| 8495 |
"node": ">=0.10.0"
|
| 8496 |
}
|
| 8497 |
},
|
| 8498 |
+
"node_modules/rolldown": {
|
| 8499 |
+
"version": "1.1.5",
|
| 8500 |
+
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
|
| 8501 |
+
"integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
|
| 8502 |
+
"dev": true,
|
| 8503 |
+
"license": "MIT",
|
| 8504 |
+
"dependencies": {
|
| 8505 |
+
"@oxc-project/types": "=0.139.0",
|
| 8506 |
+
"@rolldown/pluginutils": "^1.0.0"
|
| 8507 |
+
},
|
| 8508 |
+
"bin": {
|
| 8509 |
+
"rolldown": "bin/cli.mjs"
|
| 8510 |
+
},
|
| 8511 |
+
"engines": {
|
| 8512 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 8513 |
+
},
|
| 8514 |
+
"optionalDependencies": {
|
| 8515 |
+
"@rolldown/binding-android-arm64": "1.1.5",
|
| 8516 |
+
"@rolldown/binding-darwin-arm64": "1.1.5",
|
| 8517 |
+
"@rolldown/binding-darwin-x64": "1.1.5",
|
| 8518 |
+
"@rolldown/binding-freebsd-x64": "1.1.5",
|
| 8519 |
+
"@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
|
| 8520 |
+
"@rolldown/binding-linux-arm64-gnu": "1.1.5",
|
| 8521 |
+
"@rolldown/binding-linux-arm64-musl": "1.1.5",
|
| 8522 |
+
"@rolldown/binding-linux-ppc64-gnu": "1.1.5",
|
| 8523 |
+
"@rolldown/binding-linux-s390x-gnu": "1.1.5",
|
| 8524 |
+
"@rolldown/binding-linux-x64-gnu": "1.1.5",
|
| 8525 |
+
"@rolldown/binding-linux-x64-musl": "1.1.5",
|
| 8526 |
+
"@rolldown/binding-openharmony-arm64": "1.1.5",
|
| 8527 |
+
"@rolldown/binding-wasm32-wasi": "1.1.5",
|
| 8528 |
+
"@rolldown/binding-win32-arm64-msvc": "1.1.5",
|
| 8529 |
+
"@rolldown/binding-win32-x64-msvc": "1.1.5"
|
| 8530 |
+
}
|
| 8531 |
+
},
|
| 8532 |
"node_modules/run-parallel": {
|
| 8533 |
"version": "1.2.0",
|
| 8534 |
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
|
|
|
| 8608 |
"url": "https://github.com/sponsors/ljharb"
|
| 8609 |
}
|
| 8610 |
},
|
| 8611 |
+
"node_modules/saxes": {
|
| 8612 |
+
"version": "6.0.0",
|
| 8613 |
+
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
| 8614 |
+
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
| 8615 |
+
"dev": true,
|
| 8616 |
+
"license": "ISC",
|
| 8617 |
+
"dependencies": {
|
| 8618 |
+
"xmlchars": "^2.2.0"
|
| 8619 |
+
},
|
| 8620 |
+
"engines": {
|
| 8621 |
+
"node": ">=v12.22.7"
|
| 8622 |
+
}
|
| 8623 |
+
},
|
| 8624 |
"node_modules/scheduler": {
|
| 8625 |
"version": "0.27.0",
|
| 8626 |
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
|
|
|
| 8843 |
"url": "https://github.com/sponsors/ljharb"
|
| 8844 |
}
|
| 8845 |
},
|
| 8846 |
+
"node_modules/siginfo": {
|
| 8847 |
+
"version": "2.0.0",
|
| 8848 |
+
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
| 8849 |
+
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
| 8850 |
+
"dev": true,
|
| 8851 |
+
"license": "ISC"
|
| 8852 |
+
},
|
| 8853 |
"node_modules/source-map-js": {
|
| 8854 |
"version": "1.2.1",
|
| 8855 |
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
|
|
|
| 8876 |
"dev": true,
|
| 8877 |
"license": "MIT"
|
| 8878 |
},
|
| 8879 |
+
"node_modules/stackback": {
|
| 8880 |
+
"version": "0.0.2",
|
| 8881 |
+
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
| 8882 |
+
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
| 8883 |
+
"dev": true,
|
| 8884 |
+
"license": "MIT"
|
| 8885 |
+
},
|
| 8886 |
+
"node_modules/std-env": {
|
| 8887 |
+
"version": "4.2.0",
|
| 8888 |
+
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
|
| 8889 |
+
"integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
|
| 8890 |
+
"dev": true,
|
| 8891 |
+
"license": "MIT"
|
| 8892 |
+
},
|
| 8893 |
"node_modules/stop-iteration-iterator": {
|
| 8894 |
"version": "1.1.0",
|
| 8895 |
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
|
|
|
| 9042 |
"node": ">=4"
|
| 9043 |
}
|
| 9044 |
},
|
| 9045 |
+
"node_modules/strip-indent": {
|
| 9046 |
+
"version": "3.0.0",
|
| 9047 |
+
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
|
| 9048 |
+
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
|
| 9049 |
+
"dev": true,
|
| 9050 |
+
"license": "MIT",
|
| 9051 |
+
"dependencies": {
|
| 9052 |
+
"min-indent": "^1.0.0"
|
| 9053 |
+
},
|
| 9054 |
+
"engines": {
|
| 9055 |
+
"node": ">=8"
|
| 9056 |
+
}
|
| 9057 |
+
},
|
| 9058 |
"node_modules/strip-json-comments": {
|
| 9059 |
"version": "3.1.1",
|
| 9060 |
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
|
|
|
| 9148 |
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
| 9149 |
}
|
| 9150 |
},
|
| 9151 |
+
"node_modules/symbol-tree": {
|
| 9152 |
+
"version": "3.2.4",
|
| 9153 |
+
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
| 9154 |
+
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
| 9155 |
+
"dev": true,
|
| 9156 |
+
"license": "MIT"
|
| 9157 |
+
},
|
| 9158 |
"node_modules/tailwindcss": {
|
| 9159 |
"version": "4.3.2",
|
| 9160 |
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz",
|
|
|
|
| 9188 |
"url": "https://github.com/sponsors/sindresorhus"
|
| 9189 |
}
|
| 9190 |
},
|
| 9191 |
+
"node_modules/tinybench": {
|
| 9192 |
+
"version": "2.9.0",
|
| 9193 |
+
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
| 9194 |
+
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
| 9195 |
+
"dev": true,
|
| 9196 |
+
"license": "MIT"
|
| 9197 |
+
},
|
| 9198 |
+
"node_modules/tinyexec": {
|
| 9199 |
+
"version": "1.2.4",
|
| 9200 |
+
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
|
| 9201 |
+
"integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
|
| 9202 |
+
"dev": true,
|
| 9203 |
+
"license": "MIT",
|
| 9204 |
+
"engines": {
|
| 9205 |
+
"node": ">=18"
|
| 9206 |
+
}
|
| 9207 |
+
},
|
| 9208 |
"node_modules/tinyglobby": {
|
| 9209 |
"version": "0.2.17",
|
| 9210 |
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
|
|
|
| 9253 |
"url": "https://github.com/sponsors/jonschlinkert"
|
| 9254 |
}
|
| 9255 |
},
|
| 9256 |
+
"node_modules/tinyrainbow": {
|
| 9257 |
+
"version": "3.1.0",
|
| 9258 |
+
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
|
| 9259 |
+
"integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
|
| 9260 |
+
"dev": true,
|
| 9261 |
+
"license": "MIT",
|
| 9262 |
+
"engines": {
|
| 9263 |
+
"node": ">=14.0.0"
|
| 9264 |
+
}
|
| 9265 |
+
},
|
| 9266 |
+
"node_modules/tldts": {
|
| 9267 |
+
"version": "7.4.9",
|
| 9268 |
+
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.9.tgz",
|
| 9269 |
+
"integrity": "sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==",
|
| 9270 |
+
"dev": true,
|
| 9271 |
+
"license": "MIT",
|
| 9272 |
+
"dependencies": {
|
| 9273 |
+
"tldts-core": "^7.4.9"
|
| 9274 |
+
},
|
| 9275 |
+
"bin": {
|
| 9276 |
+
"tldts": "bin/cli.js"
|
| 9277 |
+
}
|
| 9278 |
+
},
|
| 9279 |
+
"node_modules/tldts-core": {
|
| 9280 |
+
"version": "7.4.9",
|
| 9281 |
+
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.9.tgz",
|
| 9282 |
+
"integrity": "sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==",
|
| 9283 |
+
"dev": true,
|
| 9284 |
+
"license": "MIT"
|
| 9285 |
+
},
|
| 9286 |
"node_modules/to-regex-range": {
|
| 9287 |
"version": "5.0.1",
|
| 9288 |
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
|
|
|
| 9296 |
"node": ">=8.0"
|
| 9297 |
}
|
| 9298 |
},
|
| 9299 |
+
"node_modules/tough-cookie": {
|
| 9300 |
+
"version": "6.0.2",
|
| 9301 |
+
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz",
|
| 9302 |
+
"integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==",
|
| 9303 |
+
"dev": true,
|
| 9304 |
+
"license": "BSD-3-Clause",
|
| 9305 |
+
"dependencies": {
|
| 9306 |
+
"tldts": "^7.0.5"
|
| 9307 |
+
},
|
| 9308 |
+
"engines": {
|
| 9309 |
+
"node": ">=16"
|
| 9310 |
+
}
|
| 9311 |
+
},
|
| 9312 |
+
"node_modules/tr46": {
|
| 9313 |
+
"version": "6.0.0",
|
| 9314 |
+
"resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
|
| 9315 |
+
"integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
|
| 9316 |
+
"dev": true,
|
| 9317 |
+
"license": "MIT",
|
| 9318 |
+
"dependencies": {
|
| 9319 |
+
"punycode": "^2.3.1"
|
| 9320 |
+
},
|
| 9321 |
+
"engines": {
|
| 9322 |
+
"node": ">=20"
|
| 9323 |
+
}
|
| 9324 |
+
},
|
| 9325 |
"node_modules/trim-lines": {
|
| 9326 |
"version": "3.0.1",
|
| 9327 |
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
|
|
|
|
| 9535 |
"url": "https://github.com/sponsors/ljharb"
|
| 9536 |
}
|
| 9537 |
},
|
| 9538 |
+
"node_modules/undici": {
|
| 9539 |
+
"version": "7.28.0",
|
| 9540 |
+
"resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
|
| 9541 |
+
"integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
|
| 9542 |
+
"dev": true,
|
| 9543 |
+
"license": "MIT",
|
| 9544 |
+
"engines": {
|
| 9545 |
+
"node": ">=20.18.1"
|
| 9546 |
+
}
|
| 9547 |
+
},
|
| 9548 |
"node_modules/undici-types": {
|
| 9549 |
"version": "7.18.2",
|
| 9550 |
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
|
|
|
| 9769 |
"url": "https://opencollective.com/unified"
|
| 9770 |
}
|
| 9771 |
},
|
| 9772 |
+
"node_modules/vite": {
|
| 9773 |
+
"version": "8.1.5",
|
| 9774 |
+
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
|
| 9775 |
+
"integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
|
| 9776 |
+
"dev": true,
|
| 9777 |
+
"license": "MIT",
|
| 9778 |
+
"dependencies": {
|
| 9779 |
+
"lightningcss": "^1.32.0",
|
| 9780 |
+
"picomatch": "^4.0.5",
|
| 9781 |
+
"postcss": "^8.5.17",
|
| 9782 |
+
"rolldown": "~1.1.5",
|
| 9783 |
+
"tinyglobby": "^0.2.17"
|
| 9784 |
+
},
|
| 9785 |
+
"bin": {
|
| 9786 |
+
"vite": "bin/vite.js"
|
| 9787 |
+
},
|
| 9788 |
+
"engines": {
|
| 9789 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 9790 |
+
},
|
| 9791 |
+
"funding": {
|
| 9792 |
+
"url": "https://github.com/vitejs/vite?sponsor=1"
|
| 9793 |
+
},
|
| 9794 |
+
"optionalDependencies": {
|
| 9795 |
+
"fsevents": "~2.3.3"
|
| 9796 |
+
},
|
| 9797 |
+
"peerDependencies": {
|
| 9798 |
+
"@types/node": "^20.19.0 || >=22.12.0",
|
| 9799 |
+
"@vitejs/devtools": "^0.3.0",
|
| 9800 |
+
"esbuild": "^0.27.0 || ^0.28.0",
|
| 9801 |
+
"jiti": ">=1.21.0",
|
| 9802 |
+
"less": "^4.0.0",
|
| 9803 |
+
"sass": "^1.70.0",
|
| 9804 |
+
"sass-embedded": "^1.70.0",
|
| 9805 |
+
"stylus": ">=0.54.8",
|
| 9806 |
+
"sugarss": "^5.0.0",
|
| 9807 |
+
"terser": "^5.16.0",
|
| 9808 |
+
"tsx": "^4.8.1",
|
| 9809 |
+
"yaml": "^2.4.2"
|
| 9810 |
+
},
|
| 9811 |
+
"peerDependenciesMeta": {
|
| 9812 |
+
"@types/node": {
|
| 9813 |
+
"optional": true
|
| 9814 |
+
},
|
| 9815 |
+
"@vitejs/devtools": {
|
| 9816 |
+
"optional": true
|
| 9817 |
+
},
|
| 9818 |
+
"esbuild": {
|
| 9819 |
+
"optional": true
|
| 9820 |
+
},
|
| 9821 |
+
"jiti": {
|
| 9822 |
+
"optional": true
|
| 9823 |
+
},
|
| 9824 |
+
"less": {
|
| 9825 |
+
"optional": true
|
| 9826 |
+
},
|
| 9827 |
+
"sass": {
|
| 9828 |
+
"optional": true
|
| 9829 |
+
},
|
| 9830 |
+
"sass-embedded": {
|
| 9831 |
+
"optional": true
|
| 9832 |
+
},
|
| 9833 |
+
"stylus": {
|
| 9834 |
+
"optional": true
|
| 9835 |
+
},
|
| 9836 |
+
"sugarss": {
|
| 9837 |
+
"optional": true
|
| 9838 |
+
},
|
| 9839 |
+
"terser": {
|
| 9840 |
+
"optional": true
|
| 9841 |
+
},
|
| 9842 |
+
"tsx": {
|
| 9843 |
+
"optional": true
|
| 9844 |
+
},
|
| 9845 |
+
"yaml": {
|
| 9846 |
+
"optional": true
|
| 9847 |
+
}
|
| 9848 |
+
}
|
| 9849 |
+
},
|
| 9850 |
+
"node_modules/vite/node_modules/picomatch": {
|
| 9851 |
+
"version": "4.0.5",
|
| 9852 |
+
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
|
| 9853 |
+
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
| 9854 |
+
"dev": true,
|
| 9855 |
+
"license": "MIT",
|
| 9856 |
+
"engines": {
|
| 9857 |
+
"node": ">=12"
|
| 9858 |
+
},
|
| 9859 |
+
"funding": {
|
| 9860 |
+
"url": "https://github.com/sponsors/jonschlinkert"
|
| 9861 |
+
}
|
| 9862 |
+
},
|
| 9863 |
+
"node_modules/vitest": {
|
| 9864 |
+
"version": "4.1.10",
|
| 9865 |
+
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz",
|
| 9866 |
+
"integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==",
|
| 9867 |
+
"dev": true,
|
| 9868 |
+
"license": "MIT",
|
| 9869 |
+
"dependencies": {
|
| 9870 |
+
"@vitest/expect": "4.1.10",
|
| 9871 |
+
"@vitest/mocker": "4.1.10",
|
| 9872 |
+
"@vitest/pretty-format": "4.1.10",
|
| 9873 |
+
"@vitest/runner": "4.1.10",
|
| 9874 |
+
"@vitest/snapshot": "4.1.10",
|
| 9875 |
+
"@vitest/spy": "4.1.10",
|
| 9876 |
+
"@vitest/utils": "4.1.10",
|
| 9877 |
+
"es-module-lexer": "^2.0.0",
|
| 9878 |
+
"expect-type": "^1.3.0",
|
| 9879 |
+
"magic-string": "^0.30.21",
|
| 9880 |
+
"obug": "^2.1.1",
|
| 9881 |
+
"pathe": "^2.0.3",
|
| 9882 |
+
"picomatch": "^4.0.3",
|
| 9883 |
+
"std-env": "^4.0.0-rc.1",
|
| 9884 |
+
"tinybench": "^2.9.0",
|
| 9885 |
+
"tinyexec": "^1.0.2",
|
| 9886 |
+
"tinyglobby": "^0.2.15",
|
| 9887 |
+
"tinyrainbow": "^3.1.0",
|
| 9888 |
+
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
|
| 9889 |
+
"why-is-node-running": "^2.3.0"
|
| 9890 |
+
},
|
| 9891 |
+
"bin": {
|
| 9892 |
+
"vitest": "vitest.mjs"
|
| 9893 |
+
},
|
| 9894 |
+
"engines": {
|
| 9895 |
+
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
|
| 9896 |
+
},
|
| 9897 |
+
"funding": {
|
| 9898 |
+
"url": "https://opencollective.com/vitest"
|
| 9899 |
+
},
|
| 9900 |
+
"peerDependencies": {
|
| 9901 |
+
"@edge-runtime/vm": "*",
|
| 9902 |
+
"@opentelemetry/api": "^1.9.0",
|
| 9903 |
+
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
| 9904 |
+
"@vitest/browser-playwright": "4.1.10",
|
| 9905 |
+
"@vitest/browser-preview": "4.1.10",
|
| 9906 |
+
"@vitest/browser-webdriverio": "4.1.10",
|
| 9907 |
+
"@vitest/coverage-istanbul": "4.1.10",
|
| 9908 |
+
"@vitest/coverage-v8": "4.1.10",
|
| 9909 |
+
"@vitest/ui": "4.1.10",
|
| 9910 |
+
"happy-dom": "*",
|
| 9911 |
+
"jsdom": "*",
|
| 9912 |
+
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
| 9913 |
+
},
|
| 9914 |
+
"peerDependenciesMeta": {
|
| 9915 |
+
"@edge-runtime/vm": {
|
| 9916 |
+
"optional": true
|
| 9917 |
+
},
|
| 9918 |
+
"@opentelemetry/api": {
|
| 9919 |
+
"optional": true
|
| 9920 |
+
},
|
| 9921 |
+
"@types/node": {
|
| 9922 |
+
"optional": true
|
| 9923 |
+
},
|
| 9924 |
+
"@vitest/browser-playwright": {
|
| 9925 |
+
"optional": true
|
| 9926 |
+
},
|
| 9927 |
+
"@vitest/browser-preview": {
|
| 9928 |
+
"optional": true
|
| 9929 |
+
},
|
| 9930 |
+
"@vitest/browser-webdriverio": {
|
| 9931 |
+
"optional": true
|
| 9932 |
+
},
|
| 9933 |
+
"@vitest/coverage-istanbul": {
|
| 9934 |
+
"optional": true
|
| 9935 |
+
},
|
| 9936 |
+
"@vitest/coverage-v8": {
|
| 9937 |
+
"optional": true
|
| 9938 |
+
},
|
| 9939 |
+
"@vitest/ui": {
|
| 9940 |
+
"optional": true
|
| 9941 |
+
},
|
| 9942 |
+
"happy-dom": {
|
| 9943 |
+
"optional": true
|
| 9944 |
+
},
|
| 9945 |
+
"jsdom": {
|
| 9946 |
+
"optional": true
|
| 9947 |
+
},
|
| 9948 |
+
"vite": {
|
| 9949 |
+
"optional": false
|
| 9950 |
+
}
|
| 9951 |
+
}
|
| 9952 |
+
},
|
| 9953 |
+
"node_modules/vitest/node_modules/picomatch": {
|
| 9954 |
+
"version": "4.0.5",
|
| 9955 |
+
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
|
| 9956 |
+
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
| 9957 |
+
"dev": true,
|
| 9958 |
+
"license": "MIT",
|
| 9959 |
+
"engines": {
|
| 9960 |
+
"node": ">=12"
|
| 9961 |
+
},
|
| 9962 |
+
"funding": {
|
| 9963 |
+
"url": "https://github.com/sponsors/jonschlinkert"
|
| 9964 |
+
}
|
| 9965 |
+
},
|
| 9966 |
+
"node_modules/w3c-xmlserializer": {
|
| 9967 |
+
"version": "5.0.0",
|
| 9968 |
+
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
| 9969 |
+
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
| 9970 |
+
"dev": true,
|
| 9971 |
+
"license": "MIT",
|
| 9972 |
+
"dependencies": {
|
| 9973 |
+
"xml-name-validator": "^5.0.0"
|
| 9974 |
+
},
|
| 9975 |
+
"engines": {
|
| 9976 |
+
"node": ">=18"
|
| 9977 |
+
}
|
| 9978 |
+
},
|
| 9979 |
+
"node_modules/webidl-conversions": {
|
| 9980 |
+
"version": "8.0.1",
|
| 9981 |
+
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
|
| 9982 |
+
"integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
|
| 9983 |
+
"dev": true,
|
| 9984 |
+
"license": "BSD-2-Clause",
|
| 9985 |
+
"engines": {
|
| 9986 |
+
"node": ">=20"
|
| 9987 |
+
}
|
| 9988 |
+
},
|
| 9989 |
+
"node_modules/whatwg-mimetype": {
|
| 9990 |
+
"version": "5.0.0",
|
| 9991 |
+
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
|
| 9992 |
+
"integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
|
| 9993 |
+
"dev": true,
|
| 9994 |
+
"license": "MIT",
|
| 9995 |
+
"engines": {
|
| 9996 |
+
"node": ">=20"
|
| 9997 |
+
}
|
| 9998 |
+
},
|
| 9999 |
+
"node_modules/whatwg-url": {
|
| 10000 |
+
"version": "16.0.1",
|
| 10001 |
+
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
|
| 10002 |
+
"integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
|
| 10003 |
+
"dev": true,
|
| 10004 |
+
"license": "MIT",
|
| 10005 |
+
"dependencies": {
|
| 10006 |
+
"@exodus/bytes": "^1.11.0",
|
| 10007 |
+
"tr46": "^6.0.0",
|
| 10008 |
+
"webidl-conversions": "^8.0.1"
|
| 10009 |
+
},
|
| 10010 |
+
"engines": {
|
| 10011 |
+
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
| 10012 |
+
}
|
| 10013 |
+
},
|
| 10014 |
"node_modules/which": {
|
| 10015 |
"version": "2.0.2",
|
| 10016 |
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
|
|
|
| 10116 |
"url": "https://github.com/sponsors/ljharb"
|
| 10117 |
}
|
| 10118 |
},
|
| 10119 |
+
"node_modules/why-is-node-running": {
|
| 10120 |
+
"version": "2.3.0",
|
| 10121 |
+
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
| 10122 |
+
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
| 10123 |
+
"dev": true,
|
| 10124 |
+
"license": "MIT",
|
| 10125 |
+
"dependencies": {
|
| 10126 |
+
"siginfo": "^2.0.0",
|
| 10127 |
+
"stackback": "0.0.2"
|
| 10128 |
+
},
|
| 10129 |
+
"bin": {
|
| 10130 |
+
"why-is-node-running": "cli.js"
|
| 10131 |
+
},
|
| 10132 |
+
"engines": {
|
| 10133 |
+
"node": ">=8"
|
| 10134 |
+
}
|
| 10135 |
+
},
|
| 10136 |
"node_modules/word-wrap": {
|
| 10137 |
"version": "1.2.5",
|
| 10138 |
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
|
|
|
| 10143 |
"node": ">=0.10.0"
|
| 10144 |
}
|
| 10145 |
},
|
| 10146 |
+
"node_modules/xml-name-validator": {
|
| 10147 |
+
"version": "5.0.0",
|
| 10148 |
+
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
| 10149 |
+
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
| 10150 |
+
"dev": true,
|
| 10151 |
+
"license": "Apache-2.0",
|
| 10152 |
+
"engines": {
|
| 10153 |
+
"node": ">=18"
|
| 10154 |
+
}
|
| 10155 |
+
},
|
| 10156 |
+
"node_modules/xmlchars": {
|
| 10157 |
+
"version": "2.2.0",
|
| 10158 |
+
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
| 10159 |
+
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
| 10160 |
+
"dev": true,
|
| 10161 |
+
"license": "MIT"
|
| 10162 |
+
},
|
| 10163 |
"node_modules/yallist": {
|
| 10164 |
"version": "3.1.1",
|
| 10165 |
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
frontend/package.json
CHANGED
|
@@ -8,7 +8,11 @@
|
|
| 8 |
"dev": "next dev",
|
| 9 |
"build": "next build",
|
| 10 |
"start": "next start",
|
| 11 |
-
"test": "
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
"lint": "eslint ."
|
| 13 |
},
|
| 14 |
"dependencies": {
|
|
@@ -26,13 +30,19 @@
|
|
| 26 |
},
|
| 27 |
"devDependencies": {
|
| 28 |
"@eslint/eslintrc": "^3.3.1",
|
|
|
|
| 29 |
"@tailwindcss/postcss": "^4.1.17",
|
|
|
|
|
|
|
|
|
|
| 30 |
"@types/node": "^24.10.1",
|
| 31 |
"@types/react": "^19.2.2",
|
| 32 |
"@types/react-dom": "^19.2.2",
|
| 33 |
"eslint": "^9.39.4",
|
| 34 |
"eslint-config-next": "^16.2.3",
|
|
|
|
| 35 |
"tailwindcss": "^4.2.2",
|
| 36 |
-
"typescript": "^6.0.3"
|
|
|
|
| 37 |
}
|
| 38 |
}
|
|
|
|
| 8 |
"dev": "next dev",
|
| 9 |
"build": "next build",
|
| 10 |
"start": "next start",
|
| 11 |
+
"test": "npm run test:unit && npm run test:components",
|
| 12 |
+
"test:unit": "node --test lib/*.test.mts",
|
| 13 |
+
"test:components": "vitest run",
|
| 14 |
+
"test:e2e": "playwright test",
|
| 15 |
+
"test:watch": "vitest",
|
| 16 |
"lint": "eslint ."
|
| 17 |
},
|
| 18 |
"dependencies": {
|
|
|
|
| 30 |
},
|
| 31 |
"devDependencies": {
|
| 32 |
"@eslint/eslintrc": "^3.3.1",
|
| 33 |
+
"@playwright/test": "^1.61.1",
|
| 34 |
"@tailwindcss/postcss": "^4.1.17",
|
| 35 |
+
"@testing-library/jest-dom": "^6.9.1",
|
| 36 |
+
"@testing-library/react": "^16.3.2",
|
| 37 |
+
"@testing-library/user-event": "^14.6.1",
|
| 38 |
"@types/node": "^24.10.1",
|
| 39 |
"@types/react": "^19.2.2",
|
| 40 |
"@types/react-dom": "^19.2.2",
|
| 41 |
"eslint": "^9.39.4",
|
| 42 |
"eslint-config-next": "^16.2.3",
|
| 43 |
+
"jsdom": "^29.1.1",
|
| 44 |
"tailwindcss": "^4.2.2",
|
| 45 |
+
"typescript": "^6.0.3",
|
| 46 |
+
"vitest": "^4.1.10"
|
| 47 |
}
|
| 48 |
}
|
frontend/playwright.config.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig, devices } from "@playwright/test";
|
| 2 |
+
|
| 3 |
+
export default defineConfig({
|
| 4 |
+
testDir: "./e2e",
|
| 5 |
+
outputDir: "../output/playwright/test-results",
|
| 6 |
+
fullyParallel: false,
|
| 7 |
+
forbidOnly: Boolean(process.env.CI),
|
| 8 |
+
retries: process.env.CI ? 1 : 0,
|
| 9 |
+
reporter: process.env.CI ? "github" : "list",
|
| 10 |
+
use: {
|
| 11 |
+
baseURL: "http://127.0.0.1:3100",
|
| 12 |
+
screenshot: "only-on-failure",
|
| 13 |
+
trace: "retain-on-failure",
|
| 14 |
+
},
|
| 15 |
+
projects: [
|
| 16 |
+
{
|
| 17 |
+
name: "chromium",
|
| 18 |
+
use: { ...devices["Desktop Chrome"] },
|
| 19 |
+
},
|
| 20 |
+
],
|
| 21 |
+
webServer: {
|
| 22 |
+
command: "python3 -m http.server 3100 --bind 127.0.0.1 --directory out",
|
| 23 |
+
url: "http://127.0.0.1:3100",
|
| 24 |
+
reuseExistingServer: !process.env.CI,
|
| 25 |
+
timeout: 120_000,
|
| 26 |
+
},
|
| 27 |
+
});
|
frontend/test/setup.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import "@testing-library/jest-dom/vitest";
|
| 2 |
+
|
| 3 |
+
import { cleanup } from "@testing-library/react";
|
| 4 |
+
import { afterEach, vi } from "vitest";
|
| 5 |
+
|
| 6 |
+
afterEach(() => {
|
| 7 |
+
cleanup();
|
| 8 |
+
vi.restoreAllMocks();
|
| 9 |
+
});
|
| 10 |
+
|
| 11 |
+
if (!HTMLElement.prototype.scrollIntoView) {
|
| 12 |
+
HTMLElement.prototype.scrollIntoView = vi.fn();
|
| 13 |
+
}
|
frontend/vitest.config.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { fileURLToPath, URL } from "node:url";
|
| 2 |
+
import { defineConfig } from "vitest/config";
|
| 3 |
+
|
| 4 |
+
export default defineConfig({
|
| 5 |
+
resolve: {
|
| 6 |
+
alias: {
|
| 7 |
+
"@": fileURLToPath(new URL(".", import.meta.url)),
|
| 8 |
+
},
|
| 9 |
+
},
|
| 10 |
+
test: {
|
| 11 |
+
environment: "jsdom",
|
| 12 |
+
globals: true,
|
| 13 |
+
include: ["components/**/*.test.tsx"],
|
| 14 |
+
setupFiles: ["./test/setup.ts"],
|
| 15 |
+
},
|
| 16 |
+
});
|