newpr 0.1.0 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli/args.ts +6 -1
- package/src/cli/index.ts +11 -2
- package/src/cli/update-check.ts +76 -0
- package/src/llm/cartoon.ts +128 -0
- package/src/types/output.ts +7 -0
- package/src/web/client/App.tsx +4 -0
- package/src/web/client/components/ResultsScreen.tsx +110 -80
- package/src/web/client/hooks/useFeatures.ts +18 -0
- package/src/web/client/panels/CartoonPanel.tsx +96 -0
- package/src/web/server/routes.ts +49 -1
- package/src/web/server.ts +9 -2
- package/src/web/styles/built.css +2 -0
package/package.json
CHANGED
package/src/cli/args.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface CliArgs {
|
|
|
10
10
|
noClone: boolean;
|
|
11
11
|
agent?: AgentToolName;
|
|
12
12
|
port?: number;
|
|
13
|
+
cartoon?: boolean;
|
|
13
14
|
subArgs: string[];
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -101,8 +102,12 @@ export function parseArgs(argv: string[]): CliArgs {
|
|
|
101
102
|
const portIdx = args.indexOf("--port");
|
|
102
103
|
if (portIdx !== -1 && args[portIdx + 1]) {
|
|
103
104
|
port = Number.parseInt(args[portIdx + 1]!, 10) || 3000;
|
|
105
|
+
} else {
|
|
106
|
+
const eqArg = args.find((a) => a.startsWith("--port="));
|
|
107
|
+
if (eqArg) port = Number.parseInt(eqArg.split("=")[1]!, 10) || 3000;
|
|
104
108
|
}
|
|
105
|
-
|
|
109
|
+
const cartoon = args.includes("--cartoon");
|
|
110
|
+
return { command: "web", port, cartoon, ...DEFAULTS };
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
if (args.length === 0) {
|
package/src/cli/index.ts
CHANGED
|
@@ -9,12 +9,17 @@ import { parsePrInput } from "../github/parse-pr.ts";
|
|
|
9
9
|
import { analyzePr } from "../analyzer/pipeline.ts";
|
|
10
10
|
import { createStderrProgress, createSilentProgress, createStreamJsonProgress } from "../analyzer/progress.ts";
|
|
11
11
|
import { renderLoading, renderShell } from "../tui/render.tsx";
|
|
12
|
+
import { checkForUpdate, printUpdateNotice } from "./update-check.ts";
|
|
12
13
|
|
|
13
|
-
const VERSION = "0.1.
|
|
14
|
+
const VERSION = "0.1.3";
|
|
14
15
|
|
|
15
16
|
async function main(): Promise<void> {
|
|
16
17
|
const args = parseArgs(process.argv);
|
|
17
18
|
|
|
19
|
+
const updatePromise = (args.command === "shell" || args.command === "web")
|
|
20
|
+
? checkForUpdate(VERSION).catch(() => null)
|
|
21
|
+
: null;
|
|
22
|
+
|
|
18
23
|
if (args.command === "help") return;
|
|
19
24
|
|
|
20
25
|
if (args.command === "version") {
|
|
@@ -48,8 +53,10 @@ async function main(): Promise<void> {
|
|
|
48
53
|
try {
|
|
49
54
|
const config = await loadConfig({ model: args.model });
|
|
50
55
|
const token = await getGithubToken();
|
|
56
|
+
const updateInfo = await updatePromise;
|
|
57
|
+
if (updateInfo) printUpdateNotice(updateInfo);
|
|
51
58
|
const { startWebServer } = await import("../web/server.ts");
|
|
52
|
-
await startWebServer({ port: args.port ?? 3000, token, config });
|
|
59
|
+
await startWebServer({ port: args.port ?? 3000, token, config, cartoon: args.cartoon });
|
|
53
60
|
} catch (error) {
|
|
54
61
|
const message = error instanceof Error ? error.message : String(error);
|
|
55
62
|
process.stderr.write(`Error: ${message}\n`);
|
|
@@ -62,6 +69,8 @@ async function main(): Promise<void> {
|
|
|
62
69
|
try {
|
|
63
70
|
const config = await loadConfig({ model: args.model });
|
|
64
71
|
const token = await getGithubToken();
|
|
72
|
+
const updateInfo = await updatePromise;
|
|
73
|
+
if (updateInfo) printUpdateNotice(updateInfo);
|
|
65
74
|
renderShell(token, config, args.prInput);
|
|
66
75
|
} catch (error) {
|
|
67
76
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const PACKAGE_NAME = "newpr";
|
|
2
|
+
const CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000;
|
|
3
|
+
|
|
4
|
+
interface UpdateInfo {
|
|
5
|
+
current: string;
|
|
6
|
+
latest: string;
|
|
7
|
+
needsUpdate: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function getLastCheckTime(): Promise<number> {
|
|
11
|
+
try {
|
|
12
|
+
const file = Bun.file(`${process.env.HOME}/.newpr/last-update-check`);
|
|
13
|
+
const text = await file.text();
|
|
14
|
+
return Number.parseInt(text.trim(), 10) || 0;
|
|
15
|
+
} catch {
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function setLastCheckTime(): Promise<void> {
|
|
21
|
+
const dir = `${process.env.HOME}/.newpr`;
|
|
22
|
+
const { mkdirSync } = await import("node:fs");
|
|
23
|
+
try { mkdirSync(dir, { recursive: true }); } catch {}
|
|
24
|
+
await Bun.write(`${dir}/last-update-check`, String(Date.now()));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function fetchLatestVersion(): Promise<string | null> {
|
|
28
|
+
try {
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
31
|
+
const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
|
|
32
|
+
signal: controller.signal,
|
|
33
|
+
headers: { Accept: "application/json" },
|
|
34
|
+
});
|
|
35
|
+
clearTimeout(timeout);
|
|
36
|
+
if (!res.ok) return null;
|
|
37
|
+
const data = await res.json() as { version?: string };
|
|
38
|
+
return data.version ?? null;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function compareVersions(current: string, latest: string): boolean {
|
|
45
|
+
const parse = (v: string) => v.replace(/^v/, "").split(".").map(Number);
|
|
46
|
+
const c = parse(current);
|
|
47
|
+
const l = parse(latest);
|
|
48
|
+
for (let i = 0; i < 3; i++) {
|
|
49
|
+
if ((l[i] ?? 0) > (c[i] ?? 0)) return true;
|
|
50
|
+
if ((l[i] ?? 0) < (c[i] ?? 0)) return false;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function checkForUpdate(currentVersion: string): Promise<UpdateInfo | null> {
|
|
56
|
+
const lastCheck = await getLastCheckTime();
|
|
57
|
+
if (Date.now() - lastCheck < CHECK_INTERVAL_MS) return null;
|
|
58
|
+
|
|
59
|
+
const latest = await fetchLatestVersion();
|
|
60
|
+
await setLastCheckTime();
|
|
61
|
+
|
|
62
|
+
if (!latest) return null;
|
|
63
|
+
if (!compareVersions(currentVersion, latest)) return null;
|
|
64
|
+
|
|
65
|
+
return { current: currentVersion, latest, needsUpdate: true };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function printUpdateNotice(info: UpdateInfo): void {
|
|
69
|
+
const msg = [
|
|
70
|
+
"",
|
|
71
|
+
` Update available: ${info.current} → \x1b[32m${info.latest}\x1b[0m`,
|
|
72
|
+
` Run \x1b[36mbun add -g ${PACKAGE_NAME}\x1b[0m to update`,
|
|
73
|
+
"",
|
|
74
|
+
].join("\n");
|
|
75
|
+
process.stderr.write(msg);
|
|
76
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { NewprOutput } from "../types/output.ts";
|
|
2
|
+
|
|
3
|
+
const CARTOON_MODEL = "google/gemini-3-pro-image-preview";
|
|
4
|
+
|
|
5
|
+
function buildCartoonPrompt(data: NewprOutput, language: string): string {
|
|
6
|
+
const { meta, summary, groups, narrative } = data;
|
|
7
|
+
const lang = language === "auto" ? "English" : language;
|
|
8
|
+
const groupList = groups.slice(0, 5).map((g) => `- ${g.name}: ${g.description.slice(0, 80)}`).join("\n");
|
|
9
|
+
const storyExcerpt = narrative
|
|
10
|
+
.replace(/\[\[(group|file):[^\]]+\]\]/g, "")
|
|
11
|
+
.split("\n")
|
|
12
|
+
.filter((l) => l.trim() && !l.startsWith("#"))
|
|
13
|
+
.slice(0, 6)
|
|
14
|
+
.join(" ")
|
|
15
|
+
.slice(0, 500);
|
|
16
|
+
|
|
17
|
+
return `Generate an image of a funny 4-panel comic strip about this Pull Request.
|
|
18
|
+
|
|
19
|
+
## PR Context
|
|
20
|
+
Title: "${meta.pr_title}"
|
|
21
|
+
Author: ${meta.author}
|
|
22
|
+
Purpose: ${summary.purpose}
|
|
23
|
+
Scope: ${summary.scope}
|
|
24
|
+
Impact: ${summary.impact}
|
|
25
|
+
Risk: ${summary.risk_level}
|
|
26
|
+
Changes: +${meta.total_additions} -${meta.total_deletions} across ${meta.total_files_changed} files
|
|
27
|
+
|
|
28
|
+
## What happened (key changes):
|
|
29
|
+
${groupList}
|
|
30
|
+
|
|
31
|
+
## Story:
|
|
32
|
+
${storyExcerpt}
|
|
33
|
+
|
|
34
|
+
## Comic Requirements:
|
|
35
|
+
- 2x2 grid, 4 panels
|
|
36
|
+
- Cute stick-figure developer characters with expressive faces and gestures
|
|
37
|
+
- Speech bubbles with SHORT, witty dialogue in ${lang}
|
|
38
|
+
- Panel 1: The developer discovers the problem or receives the task (based on the PR purpose above)
|
|
39
|
+
- Panel 2: The developer's ambitious plan or approach (based on the actual changes)
|
|
40
|
+
- Panel 3: A funny complication that reflects the real complexity (based on risk/impact)
|
|
41
|
+
- Panel 4: The resolution with a developer humor punchline
|
|
42
|
+
- The humor should be SPECIFIC to this PR's content, not generic programming jokes
|
|
43
|
+
- Make the characters expressive and the scenes detailed
|
|
44
|
+
- The image must be square (1:1 aspect ratio, 1080x1080px), suitable for Instagram
|
|
45
|
+
- Output only the image`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface CartoonResponse {
|
|
49
|
+
choices: Array<{
|
|
50
|
+
message: {
|
|
51
|
+
content: string;
|
|
52
|
+
images?: Array<{
|
|
53
|
+
image_url: { url: string };
|
|
54
|
+
}>;
|
|
55
|
+
};
|
|
56
|
+
}>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const MAX_RETRIES = 3;
|
|
60
|
+
|
|
61
|
+
export async function generateCartoon(
|
|
62
|
+
apiKey: string,
|
|
63
|
+
data: NewprOutput,
|
|
64
|
+
language: string,
|
|
65
|
+
): Promise<{ imageBase64: string; mimeType: string }> {
|
|
66
|
+
const prompt = buildCartoonPrompt(data, language);
|
|
67
|
+
|
|
68
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
69
|
+
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: {
|
|
72
|
+
Authorization: `Bearer ${apiKey}`,
|
|
73
|
+
"Content-Type": "application/json",
|
|
74
|
+
"HTTP-Referer": "https://github.com/jiwonMe/newpr",
|
|
75
|
+
"X-Title": "newpr-cartoon",
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify({
|
|
78
|
+
model: CARTOON_MODEL,
|
|
79
|
+
messages: [
|
|
80
|
+
{ role: "user", content: prompt },
|
|
81
|
+
],
|
|
82
|
+
modalities: ["image", "text"],
|
|
83
|
+
temperature: 1.0,
|
|
84
|
+
}),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (response.status === 500 || response.status === 502 || response.status === 503) {
|
|
88
|
+
if (attempt < MAX_RETRIES) {
|
|
89
|
+
await new Promise((r) => setTimeout(r, 2000 * attempt));
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
const body = await response.text();
|
|
96
|
+
throw new Error(`Cartoon generation failed (${response.status}): ${body.slice(0, 300)}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const result = await response.json() as CartoonResponse;
|
|
100
|
+
const message = result.choices?.[0]?.message;
|
|
101
|
+
|
|
102
|
+
if (message?.images?.length) {
|
|
103
|
+
const imageUrl = message.images[0]!.image_url.url;
|
|
104
|
+
const match = imageUrl.match(/^data:image\/(png|jpeg|webp|gif);base64,(.+)$/);
|
|
105
|
+
if (match) {
|
|
106
|
+
return { imageBase64: match[2]!, mimeType: `image/${match[1]}` };
|
|
107
|
+
}
|
|
108
|
+
const imgRes = await fetch(imageUrl);
|
|
109
|
+
if (imgRes.ok) {
|
|
110
|
+
const buf = await imgRes.arrayBuffer();
|
|
111
|
+
return {
|
|
112
|
+
imageBase64: Buffer.from(buf).toString("base64"),
|
|
113
|
+
mimeType: imgRes.headers.get("content-type") ?? "image/png",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (attempt < MAX_RETRIES) {
|
|
119
|
+
await new Promise((r) => setTimeout(r, 2000 * attempt));
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const raw = JSON.stringify(result).slice(0, 500);
|
|
124
|
+
throw new Error(`No image in response. Raw: ${raw}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
throw new Error("Cartoon generation failed after retries");
|
|
128
|
+
}
|
package/src/types/output.ts
CHANGED
|
@@ -50,10 +50,17 @@ export interface FileChange {
|
|
|
50
50
|
groups: string[];
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
export interface CartoonImage {
|
|
54
|
+
imageBase64: string;
|
|
55
|
+
mimeType: string;
|
|
56
|
+
generatedAt: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
53
59
|
export interface NewprOutput {
|
|
54
60
|
meta: PrMeta;
|
|
55
61
|
summary: PrSummary;
|
|
56
62
|
groups: FileGroup[];
|
|
57
63
|
files: FileChange[];
|
|
58
64
|
narrative: string;
|
|
65
|
+
cartoon?: CartoonImage;
|
|
59
66
|
}
|
package/src/web/client/App.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { useAnalysis } from "./hooks/useAnalysis.ts";
|
|
|
3
3
|
import { useTheme } from "./hooks/useTheme.ts";
|
|
4
4
|
import { useSessions } from "./hooks/useSessions.ts";
|
|
5
5
|
import { useGithubUser } from "./hooks/useGithubUser.ts";
|
|
6
|
+
import { useFeatures } from "./hooks/useFeatures.ts";
|
|
6
7
|
import { AppShell } from "./components/AppShell.tsx";
|
|
7
8
|
import { InputScreen } from "./components/InputScreen.tsx";
|
|
8
9
|
import { LoadingTimeline } from "./components/LoadingTimeline.tsx";
|
|
@@ -31,6 +32,7 @@ export function App() {
|
|
|
31
32
|
const themeCtx = useTheme();
|
|
32
33
|
const { sessions, refresh: refreshSessions } = useSessions();
|
|
33
34
|
const githubUser = useGithubUser();
|
|
35
|
+
const features = useFeatures();
|
|
34
36
|
const initialLoadDone = useRef(false);
|
|
35
37
|
const [activeId, setActiveId] = useState<string | null>(null);
|
|
36
38
|
|
|
@@ -107,6 +109,8 @@ export function App() {
|
|
|
107
109
|
onBack={handleNewAnalysis}
|
|
108
110
|
activeId={activeId}
|
|
109
111
|
onAnchorClick={handleAnchorClick}
|
|
112
|
+
cartoonEnabled={features.cartoon}
|
|
113
|
+
sessionId={analysis.sessionId}
|
|
110
114
|
/>
|
|
111
115
|
)}
|
|
112
116
|
{analysis.phase === "error" && (
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useState,
|
|
2
|
-
import { ArrowLeft, FileText, Layers, FolderTree, BookOpen, LayoutList, GitBranch, User, Files, Bot } from "lucide-react";
|
|
1
|
+
import { useState, useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import { ArrowLeft, FileText, Layers, FolderTree, BookOpen, LayoutList, GitBranch, User, Files, Bot, Sparkles } from "lucide-react";
|
|
3
3
|
import { Button } from "../../components/ui/button.tsx";
|
|
4
4
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../../components/ui/tabs.tsx";
|
|
5
5
|
import type { NewprOutput } from "../../../types/output.ts";
|
|
@@ -8,8 +8,9 @@ import { GroupsPanel } from "../panels/GroupsPanel.tsx";
|
|
|
8
8
|
import { FilesPanel } from "../panels/FilesPanel.tsx";
|
|
9
9
|
import { NarrativePanel } from "../panels/NarrativePanel.tsx";
|
|
10
10
|
import { StoryPanel } from "../panels/StoryPanel.tsx";
|
|
11
|
+
import { CartoonPanel } from "../panels/CartoonPanel.tsx";
|
|
11
12
|
|
|
12
|
-
const VALID_TABS = ["story", "summary", "groups", "files", "narrative"] as const;
|
|
13
|
+
const VALID_TABS = ["story", "summary", "groups", "files", "narrative", "cartoon"] as const;
|
|
13
14
|
type TabValue = typeof VALID_TABS[number];
|
|
14
15
|
|
|
15
16
|
function getInitialTab(): TabValue {
|
|
@@ -36,43 +37,63 @@ export function ResultsScreen({
|
|
|
36
37
|
onBack,
|
|
37
38
|
activeId,
|
|
38
39
|
onAnchorClick,
|
|
40
|
+
cartoonEnabled,
|
|
41
|
+
sessionId,
|
|
39
42
|
}: {
|
|
40
43
|
data: NewprOutput;
|
|
41
44
|
onBack: () => void;
|
|
42
45
|
activeId: string | null;
|
|
43
46
|
onAnchorClick: (kind: "group" | "file", id: string) => void;
|
|
47
|
+
cartoonEnabled?: boolean;
|
|
48
|
+
sessionId?: string | null;
|
|
44
49
|
}) {
|
|
45
50
|
const { meta, summary } = data;
|
|
46
51
|
const [tab, setTab] = useState<TabValue>(getInitialTab);
|
|
47
|
-
|
|
48
|
-
const
|
|
52
|
+
|
|
53
|
+
const stickyRef = useRef<HTMLDivElement>(null);
|
|
54
|
+
const collapsibleRef = useRef<HTMLDivElement>(null);
|
|
55
|
+
const compactRef = useRef<HTMLDivElement>(null);
|
|
49
56
|
|
|
50
57
|
useEffect(() => {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
const sticky = stickyRef.current;
|
|
59
|
+
const collapsible = collapsibleRef.current;
|
|
60
|
+
const compact = compactRef.current;
|
|
61
|
+
if (!sticky || !collapsible || !compact) return;
|
|
62
|
+
|
|
63
|
+
const scrollParent = sticky.closest("main") ?? sticky.closest("[class*=overflow-y-auto]");
|
|
64
|
+
if (!scrollParent) return;
|
|
65
|
+
|
|
66
|
+
let wasScrolled = false;
|
|
67
|
+
|
|
68
|
+
const onScroll = () => {
|
|
69
|
+
const scrolled = scrollParent.scrollTop > 0;
|
|
70
|
+
if (scrolled === wasScrolled) return;
|
|
71
|
+
wasScrolled = scrolled;
|
|
72
|
+
|
|
73
|
+
collapsible.style.maxHeight = scrolled ? "0px" : "none";
|
|
74
|
+
collapsible.style.opacity = scrolled ? "0" : "1";
|
|
75
|
+
compact.style.maxHeight = scrolled ? "40px" : "0px";
|
|
76
|
+
compact.style.opacity = scrolled ? "1" : "0";
|
|
77
|
+
sticky.classList.toggle("border-b", scrolled);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
scrollParent.addEventListener("scroll", onScroll, { passive: true });
|
|
81
|
+
return () => scrollParent.removeEventListener("scroll", onScroll);
|
|
59
82
|
}, []);
|
|
60
83
|
|
|
61
|
-
|
|
84
|
+
const handleTabChange = useCallback((value: string) => {
|
|
62
85
|
setTab(value as TabValue);
|
|
63
86
|
setTabParam(value);
|
|
64
|
-
}
|
|
87
|
+
}, []);
|
|
65
88
|
|
|
66
89
|
const repoSlug = meta.pr_url.replace(/^https?:\/\/github\.com\//, "").replace(/\/pull\//, "#");
|
|
67
90
|
|
|
68
91
|
return (
|
|
69
|
-
<
|
|
70
|
-
<div ref={
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
<>
|
|
75
|
-
<div className="flex items-center gap-3 mb-4">
|
|
92
|
+
<Tabs value={tab} onValueChange={handleTabChange} className="flex flex-col">
|
|
93
|
+
<div ref={stickyRef} className="sticky top-0 z-10 bg-background pb-2 -mx-10 px-10">
|
|
94
|
+
<div ref={collapsibleRef} className="overflow-hidden transition-[max-height,opacity] duration-200">
|
|
95
|
+
<div className="pb-3 pt-1">
|
|
96
|
+
<div className="flex items-center gap-3 mb-3">
|
|
76
97
|
<Button variant="ghost" size="icon" className="shrink-0 -ml-2" onClick={onBack}>
|
|
77
98
|
<ArrowLeft className="h-4 w-4" />
|
|
78
99
|
</Button>
|
|
@@ -89,45 +110,45 @@ export function ResultsScreen({
|
|
|
89
110
|
</span>
|
|
90
111
|
</div>
|
|
91
112
|
|
|
92
|
-
<h1 className="text-
|
|
113
|
+
<h1 className="text-lg font-bold tracking-tight mb-2 line-clamp-2">{meta.pr_title}</h1>
|
|
93
114
|
|
|
94
|
-
<div className="flex flex-wrap gap-x-
|
|
115
|
+
<div className="flex flex-wrap gap-x-4 gap-y-1">
|
|
95
116
|
<a
|
|
96
117
|
href={meta.author_url ?? `https://github.com/${meta.author}`}
|
|
97
118
|
target="_blank"
|
|
98
119
|
rel="noopener noreferrer"
|
|
99
|
-
className="flex items-center gap-1.5 text-
|
|
120
|
+
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
100
121
|
>
|
|
101
122
|
{meta.author_avatar ? (
|
|
102
|
-
<img src={meta.author_avatar} alt={meta.author} className="h-
|
|
123
|
+
<img src={meta.author_avatar} alt={meta.author} className="h-3.5 w-3.5 rounded-full" />
|
|
103
124
|
) : (
|
|
104
|
-
<User className="h-3
|
|
125
|
+
<User className="h-3 w-3" />
|
|
105
126
|
)}
|
|
106
127
|
<span>{meta.author}</span>
|
|
107
128
|
</a>
|
|
108
|
-
<div className="flex items-center gap-1.5 text-
|
|
109
|
-
<GitBranch className="h-3
|
|
110
|
-
<span className="font-mono
|
|
129
|
+
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
130
|
+
<GitBranch className="h-3 w-3" />
|
|
131
|
+
<span className="font-mono">{meta.base_branch}</span>
|
|
111
132
|
<span className="text-muted-foreground/50">←</span>
|
|
112
|
-
<span className="font-mono
|
|
133
|
+
<span className="font-mono">{meta.head_branch}</span>
|
|
113
134
|
</div>
|
|
114
|
-
<div className="flex items-center gap-1.5 text-
|
|
115
|
-
<Files className="h-3
|
|
135
|
+
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
136
|
+
<Files className="h-3 w-3" />
|
|
116
137
|
<span className="text-green-500">+{meta.total_additions}</span>
|
|
117
138
|
<span className="text-red-500">−{meta.total_deletions}</span>
|
|
118
139
|
<span className="text-muted-foreground/50">·</span>
|
|
119
140
|
<span>{meta.total_files_changed} files</span>
|
|
120
141
|
</div>
|
|
121
|
-
<div className="flex items-center gap-1.5 text-
|
|
122
|
-
<Bot className="h-3
|
|
142
|
+
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
143
|
+
<Bot className="h-3 w-3" />
|
|
123
144
|
<span>{meta.model_used.split("/").pop()}</span>
|
|
124
145
|
</div>
|
|
125
146
|
</div>
|
|
126
|
-
|
|
127
|
-
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
128
149
|
|
|
129
|
-
{
|
|
130
|
-
<div className="flex items-center gap-3 min-w-0">
|
|
150
|
+
<div ref={compactRef} className="overflow-hidden transition-[max-height,opacity] duration-200" style={{ maxHeight: 0, opacity: 0 }}>
|
|
151
|
+
<div className="flex items-center gap-3 min-w-0 pb-2">
|
|
131
152
|
<Button variant="ghost" size="icon" className="shrink-0 -ml-2 h-7 w-7" onClick={onBack}>
|
|
132
153
|
<ArrowLeft className="h-3.5 w-3.5" />
|
|
133
154
|
</Button>
|
|
@@ -137,49 +158,58 @@ export function ResultsScreen({
|
|
|
137
158
|
{summary.risk_level}
|
|
138
159
|
</span>
|
|
139
160
|
</div>
|
|
140
|
-
|
|
161
|
+
</div>
|
|
141
162
|
|
|
142
|
-
<
|
|
143
|
-
<
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
163
|
+
<TabsList className="w-full justify-start overflow-x-auto">
|
|
164
|
+
<TabsTrigger value="story" className="gap-1.5">
|
|
165
|
+
<BookOpen className="h-3.5 w-3.5 shrink-0" />
|
|
166
|
+
Story
|
|
167
|
+
</TabsTrigger>
|
|
168
|
+
<TabsTrigger value="summary" className="gap-1.5">
|
|
169
|
+
<LayoutList className="h-3.5 w-3.5 shrink-0" />
|
|
170
|
+
Summary
|
|
171
|
+
</TabsTrigger>
|
|
172
|
+
<TabsTrigger value="groups" className="gap-1.5">
|
|
173
|
+
<Layers className="h-3.5 w-3.5 shrink-0" />
|
|
174
|
+
Groups
|
|
175
|
+
</TabsTrigger>
|
|
176
|
+
<TabsTrigger value="files" className="gap-1.5">
|
|
177
|
+
<FolderTree className="h-3.5 w-3.5 shrink-0" />
|
|
178
|
+
Files
|
|
179
|
+
</TabsTrigger>
|
|
180
|
+
<TabsTrigger value="narrative" className="gap-1.5">
|
|
181
|
+
<FileText className="h-3.5 w-3.5 shrink-0" />
|
|
182
|
+
Narrative
|
|
183
|
+
</TabsTrigger>
|
|
184
|
+
{cartoonEnabled && (
|
|
185
|
+
<TabsTrigger value="cartoon" className="gap-1.5">
|
|
186
|
+
<Sparkles className="h-3.5 w-3.5 shrink-0" />
|
|
187
|
+
Comic
|
|
155
188
|
</TabsTrigger>
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
Files
|
|
159
|
-
</TabsTrigger>
|
|
160
|
-
<TabsTrigger value="narrative" className="gap-1.5">
|
|
161
|
-
<FileText className="h-3.5 w-3.5 shrink-0" />
|
|
162
|
-
Narrative
|
|
163
|
-
</TabsTrigger>
|
|
164
|
-
</TabsList>
|
|
165
|
-
|
|
166
|
-
<TabsContent value="story">
|
|
167
|
-
<StoryPanel data={data} activeId={activeId} onAnchorClick={onAnchorClick} />
|
|
168
|
-
</TabsContent>
|
|
169
|
-
<TabsContent value="summary">
|
|
170
|
-
<SummaryPanel summary={data.summary} />
|
|
171
|
-
</TabsContent>
|
|
172
|
-
<TabsContent value="groups">
|
|
173
|
-
<GroupsPanel groups={data.groups} />
|
|
174
|
-
</TabsContent>
|
|
175
|
-
<TabsContent value="files">
|
|
176
|
-
<FilesPanel files={data.files} />
|
|
177
|
-
</TabsContent>
|
|
178
|
-
<TabsContent value="narrative">
|
|
179
|
-
<NarrativePanel narrative={data.narrative} />
|
|
180
|
-
</TabsContent>
|
|
181
|
-
</Tabs>
|
|
189
|
+
)}
|
|
190
|
+
</TabsList>
|
|
182
191
|
</div>
|
|
183
|
-
|
|
192
|
+
|
|
193
|
+
<TabsContent value="story">
|
|
194
|
+
<StoryPanel data={data} activeId={activeId} onAnchorClick={onAnchorClick} />
|
|
195
|
+
</TabsContent>
|
|
196
|
+
<TabsContent value="summary">
|
|
197
|
+
<SummaryPanel summary={data.summary} />
|
|
198
|
+
</TabsContent>
|
|
199
|
+
<TabsContent value="groups">
|
|
200
|
+
<GroupsPanel groups={data.groups} />
|
|
201
|
+
</TabsContent>
|
|
202
|
+
<TabsContent value="files">
|
|
203
|
+
<FilesPanel files={data.files} />
|
|
204
|
+
</TabsContent>
|
|
205
|
+
<TabsContent value="narrative">
|
|
206
|
+
<NarrativePanel narrative={data.narrative} />
|
|
207
|
+
</TabsContent>
|
|
208
|
+
{cartoonEnabled && (
|
|
209
|
+
<TabsContent value="cartoon">
|
|
210
|
+
<CartoonPanel data={data} sessionId={sessionId} />
|
|
211
|
+
</TabsContent>
|
|
212
|
+
)}
|
|
213
|
+
</Tabs>
|
|
184
214
|
);
|
|
185
215
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
interface Features {
|
|
4
|
+
cartoon: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function useFeatures(): Features {
|
|
8
|
+
const [features, setFeatures] = useState<Features>({ cartoon: false });
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
fetch("/api/features")
|
|
12
|
+
.then((r) => r.json())
|
|
13
|
+
.then((data) => setFeatures(data as Features))
|
|
14
|
+
.catch(() => {});
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
return features;
|
|
18
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { Loader2, Sparkles, RefreshCw } from "lucide-react";
|
|
3
|
+
import { Button } from "../../components/ui/button.tsx";
|
|
4
|
+
import type { NewprOutput } from "../../../types/output.ts";
|
|
5
|
+
|
|
6
|
+
export function CartoonPanel({ data, sessionId }: { data: NewprOutput; sessionId?: string | null }) {
|
|
7
|
+
const [state, setState] = useState<"idle" | "loading" | "done" | "error">("idle");
|
|
8
|
+
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
|
9
|
+
const [error, setError] = useState<string | null>(null);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (data.cartoon) {
|
|
13
|
+
setImageUrl(`data:${data.cartoon.mimeType};base64,${data.cartoon.imageBase64}`);
|
|
14
|
+
setState("done");
|
|
15
|
+
}
|
|
16
|
+
}, [data.cartoon]);
|
|
17
|
+
|
|
18
|
+
async function generate() {
|
|
19
|
+
setState("loading");
|
|
20
|
+
setError(null);
|
|
21
|
+
try {
|
|
22
|
+
const body: Record<string, unknown> = { data };
|
|
23
|
+
if (sessionId) body.sessionId = sessionId;
|
|
24
|
+
|
|
25
|
+
const res = await fetch("/api/cartoon", {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: { "Content-Type": "application/json" },
|
|
28
|
+
body: JSON.stringify(body),
|
|
29
|
+
});
|
|
30
|
+
const result = await res.json() as { imageBase64?: string; mimeType?: string; error?: string };
|
|
31
|
+
if (result.error) throw new Error(result.error);
|
|
32
|
+
if (!result.imageBase64) throw new Error("No image returned");
|
|
33
|
+
setImageUrl(`data:${result.mimeType};base64,${result.imageBase64}`);
|
|
34
|
+
setState("done");
|
|
35
|
+
} catch (err) {
|
|
36
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
37
|
+
setState("error");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (state === "idle") {
|
|
42
|
+
return (
|
|
43
|
+
<div className="pt-6 flex flex-col items-center gap-6 py-20">
|
|
44
|
+
<Sparkles className="h-12 w-12 text-yellow-500/60" />
|
|
45
|
+
<div className="text-center space-y-2">
|
|
46
|
+
<h3 className="text-lg font-semibold">PR 4-Panel Comic</h3>
|
|
47
|
+
<p className="text-sm text-muted-foreground max-w-sm">
|
|
48
|
+
Turn this PR into a fun 4-panel comic strip. Powered by Gemini.
|
|
49
|
+
</p>
|
|
50
|
+
</div>
|
|
51
|
+
<Button onClick={generate} size="lg">
|
|
52
|
+
<Sparkles className="mr-2 h-4 w-4" />
|
|
53
|
+
Generate Comic
|
|
54
|
+
</Button>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (state === "loading") {
|
|
60
|
+
return (
|
|
61
|
+
<div className="pt-6 flex flex-col items-center gap-4 py-20">
|
|
62
|
+
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
63
|
+
<p className="text-sm text-muted-foreground">Drawing your PR comic...</p>
|
|
64
|
+
<p className="text-xs text-muted-foreground/60">This may take 10-30 seconds</p>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (state === "error") {
|
|
70
|
+
return (
|
|
71
|
+
<div className="pt-6 flex flex-col items-center gap-4 py-20">
|
|
72
|
+
<p className="text-sm text-destructive">{error}</p>
|
|
73
|
+
<Button variant="ghost" onClick={generate}>
|
|
74
|
+
<RefreshCw className="mr-2 h-3.5 w-3.5" />
|
|
75
|
+
Try again
|
|
76
|
+
</Button>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="pt-6 flex flex-col items-center gap-4">
|
|
83
|
+
{imageUrl && (
|
|
84
|
+
<img
|
|
85
|
+
src={imageUrl}
|
|
86
|
+
alt="PR 4-panel comic"
|
|
87
|
+
className="max-w-full rounded-lg border shadow-sm"
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
90
|
+
<Button variant="ghost" size="sm" onClick={generate}>
|
|
91
|
+
<RefreshCw className="mr-2 h-3.5 w-3.5" />
|
|
92
|
+
Regenerate
|
|
93
|
+
</Button>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
package/src/web/server/routes.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { NewprConfig } from "../../types/config.ts";
|
|
2
|
+
import type { NewprOutput } from "../../types/output.ts";
|
|
2
3
|
import { DEFAULT_CONFIG } from "../../types/config.ts";
|
|
3
4
|
import { listSessions, loadSession } from "../../history/store.ts";
|
|
4
5
|
import { writeStoredConfig, type StoredConfig } from "../../config/store.ts";
|
|
5
6
|
import { startAnalysis, getSession, cancelAnalysis, subscribe } from "./session-manager.ts";
|
|
7
|
+
import { generateCartoon } from "../../llm/cartoon.ts";
|
|
6
8
|
|
|
7
9
|
function json(data: unknown, status = 200): Response {
|
|
8
10
|
return new Response(JSON.stringify(data), {
|
|
@@ -11,7 +13,11 @@ function json(data: unknown, status = 200): Response {
|
|
|
11
13
|
});
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
interface RouteOptions {
|
|
17
|
+
cartoon?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createRoutes(token: string, config: NewprConfig, options: RouteOptions = {}) {
|
|
15
21
|
return {
|
|
16
22
|
"POST /api/analysis": async (req: Request) => {
|
|
17
23
|
const body = await req.json() as { pr: string };
|
|
@@ -198,5 +204,47 @@ export function createRoutes(token: string, config: NewprConfig) {
|
|
|
198
204
|
await writeStoredConfig(update);
|
|
199
205
|
return json({ ok: true });
|
|
200
206
|
},
|
|
207
|
+
|
|
208
|
+
"GET /api/features": () => {
|
|
209
|
+
return json({ cartoon: !!options.cartoon });
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
"POST /api/cartoon": async (req: Request) => {
|
|
213
|
+
if (!options.cartoon) return json({ error: "Cartoon mode not enabled. Start with --cartoon flag." }, 403);
|
|
214
|
+
if (!config.openrouter_api_key) return json({ error: "OpenRouter API key required for cartoon generation" }, 400);
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const body = await req.json() as { data?: NewprOutput; sessionId?: string };
|
|
218
|
+
let data = body.data;
|
|
219
|
+
const sessionId = body.sessionId;
|
|
220
|
+
|
|
221
|
+
if (!data && sessionId) {
|
|
222
|
+
data = await loadSession(sessionId) as NewprOutput | null ?? undefined;
|
|
223
|
+
}
|
|
224
|
+
if (!data) return json({ error: "Missing analysis data" }, 400);
|
|
225
|
+
|
|
226
|
+
const result = await generateCartoon(config.openrouter_api_key, data, config.language);
|
|
227
|
+
|
|
228
|
+
if (sessionId) {
|
|
229
|
+
const sessionData = await loadSession(sessionId);
|
|
230
|
+
if (sessionData) {
|
|
231
|
+
sessionData.cartoon = {
|
|
232
|
+
imageBase64: result.imageBase64,
|
|
233
|
+
mimeType: result.mimeType,
|
|
234
|
+
generatedAt: new Date().toISOString(),
|
|
235
|
+
};
|
|
236
|
+
const { join } = await import("node:path");
|
|
237
|
+
const { homedir } = await import("node:os");
|
|
238
|
+
const sessionsDir = join(homedir(), ".newpr", "history", "sessions");
|
|
239
|
+
await Bun.write(join(sessionsDir, `${sessionId}.json`), JSON.stringify(sessionData, null, 2));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return json(result);
|
|
244
|
+
} catch (err) {
|
|
245
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
246
|
+
return json({ error: msg }, 500);
|
|
247
|
+
}
|
|
248
|
+
},
|
|
201
249
|
};
|
|
202
250
|
}
|
package/src/web/server.ts
CHANGED
|
@@ -7,6 +7,7 @@ interface WebServerOptions {
|
|
|
7
7
|
port: number;
|
|
8
8
|
token: string;
|
|
9
9
|
config: NewprConfig;
|
|
10
|
+
cartoon?: boolean;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
function getCssPaths() {
|
|
@@ -31,8 +32,8 @@ async function buildCss(bin: string, input: string, output: string): Promise<voi
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export async function startWebServer(options: WebServerOptions): Promise<void> {
|
|
34
|
-
const { port, token, config } = options;
|
|
35
|
-
const routes = createRoutes(token, config);
|
|
35
|
+
const { port, token, config, cartoon } = options;
|
|
36
|
+
const routes = createRoutes(token, config, { cartoon });
|
|
36
37
|
const css = getCssPaths();
|
|
37
38
|
|
|
38
39
|
await buildCss(css.bin, css.input, css.output);
|
|
@@ -82,6 +83,12 @@ export async function startWebServer(options: WebServerOptions): Promise<void> {
|
|
|
82
83
|
if (path.match(/^\/api\/sessions\/[^/]+$/) && req.method === "GET") {
|
|
83
84
|
return routes["GET /api/sessions/:id"](req);
|
|
84
85
|
}
|
|
86
|
+
if (path === "/api/features" && req.method === "GET") {
|
|
87
|
+
return routes["GET /api/features"]();
|
|
88
|
+
}
|
|
89
|
+
if (path === "/api/cartoon" && req.method === "POST") {
|
|
90
|
+
return routes["POST /api/cartoon"](req);
|
|
91
|
+
}
|
|
85
92
|
|
|
86
93
|
return new Response("Not Found", { status: 404 });
|
|
87
94
|
},
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:"Pretendard","Inter",ui-sans-serif,system-ui,sans-serif;--font-mono:"JetBrains Mono",ui-monospace,monospace;--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-yellow-600:oklch(68.1% .162 75.834);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-teal-400:oklch(77.7% .152 181.912);--color-teal-500:oklch(70.4% .14 182.503);--color-teal-600:oklch(60% .118 184.704);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-600:oklch(55.8% .288 302.321);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-black:#000;--spacing:.25rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-4xl:56rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5/2.25);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wider:.05em;--leading-relaxed:1.625;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--animate-spin:spin 1s linear infinite;--blur-sm:8px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--radius:.5rem}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{border-color:var(--border)}body{background-color:var(--bg);color:var(--fg);font-feature-settings:"rlig" 1,"calt" 1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}}@layer components;@layer utilities{.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing)*0)}.top-0{top:calc(var(--spacing)*0)}.z-10{z-index:10}.z-50{z-index:50}.-mx-1{margin-inline:calc(var(--spacing)*-1)}.-mx-2{margin-inline:calc(var(--spacing)*-2)}.-mx-10{margin-inline:calc(var(--spacing)*-10)}.mx-auto{margin-inline:auto}.my-4{margin-block:calc(var(--spacing)*4)}.mt-0\.5{margin-top:calc(var(--spacing)*.5)}.mt-1\.5{margin-top:calc(var(--spacing)*1.5)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-6{margin-top:calc(var(--spacing)*6)}.mr-2{margin-right:calc(var(--spacing)*2)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.-ml-2{margin-left:calc(var(--spacing)*-2)}.ml-2{margin-left:calc(var(--spacing)*2)}.ml-4{margin-left:calc(var(--spacing)*4)}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-2{height:calc(var(--spacing)*2)}.h-2\.5{height:calc(var(--spacing)*2.5)}.h-3{height:calc(var(--spacing)*3)}.h-3\.5{height:calc(var(--spacing)*3.5)}.h-4{height:calc(var(--spacing)*4)}.h-5{height:calc(var(--spacing)*5)}.h-6{height:calc(var(--spacing)*6)}.h-7{height:calc(var(--spacing)*7)}.h-8{height:calc(var(--spacing)*8)}.h-9{height:calc(var(--spacing)*9)}.h-10{height:calc(var(--spacing)*10)}.h-11{height:calc(var(--spacing)*11)}.h-12{height:calc(var(--spacing)*12)}.h-14{height:calc(var(--spacing)*14)}.h-full{height:100%}.h-screen{height:100vh}.max-h-40{max-height:calc(var(--spacing)*40)}.max-h-\[85vh\]{max-height:85vh}.w-1{width:calc(var(--spacing)*1)}.w-2{width:calc(var(--spacing)*2)}.w-2\.5{width:calc(var(--spacing)*2.5)}.w-3{width:calc(var(--spacing)*3)}.w-3\.5{width:calc(var(--spacing)*3.5)}.w-4{width:calc(var(--spacing)*4)}.w-5{width:calc(var(--spacing)*5)}.w-6{width:calc(var(--spacing)*6)}.w-7{width:calc(var(--spacing)*7)}.w-8{width:calc(var(--spacing)*8)}.w-9{width:calc(var(--spacing)*9)}.w-10{width:calc(var(--spacing)*10)}.w-12{width:calc(var(--spacing)*12)}.w-20{width:calc(var(--spacing)*20)}.w-full{width:100%}.max-w-4xl{max-width:var(--container-4xl)}.max-w-full{max-width:100%}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-sm{max-width:var(--container-sm)}.max-w-xl{max-width:var(--container-xl)}.min-w-0{min-width:calc(var(--spacing)*0)}.flex-1{flex:1}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.border-collapse{border-collapse:collapse}.animate-spin{animation:var(--animate-spin)}.cursor-col-resize{cursor:col-resize}.cursor-pointer{cursor:pointer}.touch-none{touch-action:none}.list-decimal{list-style-type:decimal}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-0{gap:calc(var(--spacing)*0)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-2\.5{gap:calc(var(--spacing)*2.5)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}.gap-12{gap:calc(var(--spacing)*12)}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*.5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*.5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1.5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1.5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*8)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-y-reverse)))}.gap-x-4{column-gap:calc(var(--spacing)*4)}.gap-y-1{row-gap:calc(var(--spacing)*1)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:var(--radius)}.rounded-\[inherit\]{border-radius:inherit}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-border{border-color:var(--border)}.border-input{border-color:var(--input)}.border-muted-foreground\/30{border-color:var(--muted-fg)}@supports (color:color-mix(in lab, red, red)){.border-muted-foreground\/30{border-color:color-mix(in oklab,var(--muted-fg)30%,transparent)}}.border-t-transparent{border-top-color:#0000}.border-l-transparent{border-left-color:#0000}.bg-background{background-color:var(--bg)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black)50%,transparent)}}.bg-blue-500\/10{background-color:#3080ff1a}@supports (color:color-mix(in lab, red, red)){.bg-blue-500\/10{background-color:color-mix(in oklab,var(--color-blue-500)10%,transparent)}}.bg-blue-500\/20{background-color:#3080ff33}@supports (color:color-mix(in lab, red, red)){.bg-blue-500\/20{background-color:color-mix(in oklab,var(--color-blue-500)20%,transparent)}}.bg-border{background-color:var(--border)}.bg-card{background-color:var(--card)}.bg-destructive{background-color:var(--destructive)}.bg-gray-500\/10{background-color:#6a72821a}@supports (color:color-mix(in lab, red, red)){.bg-gray-500\/10{background-color:color-mix(in oklab,var(--color-gray-500)10%,transparent)}}.bg-green-500{background-color:var(--color-green-500)}.bg-green-500\/10{background-color:#00c7581a}@supports (color:color-mix(in lab, red, red)){.bg-green-500\/10{background-color:color-mix(in oklab,var(--color-green-500)10%,transparent)}}.bg-muted,.bg-muted\/50{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.bg-muted\/50{background-color:color-mix(in oklab,var(--muted)50%,transparent)}}.bg-orange-500\/10{background-color:#fe6e001a}@supports (color:color-mix(in lab, red, red)){.bg-orange-500\/10{background-color:color-mix(in oklab,var(--color-orange-500)10%,transparent)}}.bg-primary{background-color:var(--primary)}.bg-purple-500\/10{background-color:#ac4bff1a}@supports (color:color-mix(in lab, red, red)){.bg-purple-500\/10{background-color:color-mix(in oklab,var(--color-purple-500)10%,transparent)}}.bg-red-500{background-color:var(--color-red-500)}.bg-red-500\/10{background-color:#fb2c361a}@supports (color:color-mix(in lab, red, red)){.bg-red-500\/10{background-color:color-mix(in oklab,var(--color-red-500)10%,transparent)}}.bg-red-500\/20{background-color:#fb2c3633}@supports (color:color-mix(in lab, red, red)){.bg-red-500\/20{background-color:color-mix(in oklab,var(--color-red-500)20%,transparent)}}.bg-red-600{background-color:var(--color-red-600)}.bg-secondary{background-color:var(--secondary)}.bg-teal-500\/10{background-color:#00baa71a}@supports (color:color-mix(in lab, red, red)){.bg-teal-500\/10{background-color:color-mix(in oklab,var(--color-teal-500)10%,transparent)}}.bg-yellow-500{background-color:var(--color-yellow-500)}.bg-yellow-500\/10{background-color:#edb2001a}@supports (color:color-mix(in lab, red, red)){.bg-yellow-500\/10{background-color:color-mix(in oklab,var(--color-yellow-500)10%,transparent)}}.p-1{padding:calc(var(--spacing)*1)}.p-1\.5{padding:calc(var(--spacing)*1.5)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-\[1px\]{padding:1px}.px-1{padding-inline:calc(var(--spacing)*1)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-8{padding-inline:calc(var(--spacing)*8)}.px-10{padding-inline:calc(var(--spacing)*10)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-2\.5{padding-block:calc(var(--spacing)*2.5)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-6{padding-block:calc(var(--spacing)*6)}.py-10{padding-block:calc(var(--spacing)*10)}.py-16{padding-block:calc(var(--spacing)*16)}.py-20{padding-block:calc(var(--spacing)*20)}.py-24{padding-block:calc(var(--spacing)*24)}.pt-0{padding-top:calc(var(--spacing)*0)}.pt-1{padding-top:calc(var(--spacing)*1)}.pt-3{padding-top:calc(var(--spacing)*3)}.pt-4{padding-top:calc(var(--spacing)*4)}.pt-5{padding-top:calc(var(--spacing)*5)}.pt-6{padding-top:calc(var(--spacing)*6)}.pb-2{padding-bottom:calc(var(--spacing)*2)}.pb-3{padding-bottom:calc(var(--spacing)*3)}.pb-4{padding-bottom:calc(var(--spacing)*4)}.pb-6{padding-bottom:calc(var(--spacing)*6)}.pl-2{padding-left:calc(var(--spacing)*2)}.pl-4{padding-left:calc(var(--spacing)*4)}.pl-5{padding-left:calc(var(--spacing)*5)}.pl-6{padding-left:calc(var(--spacing)*6)}.pl-8{padding-left:calc(var(--spacing)*8)}.pl-12{padding-left:calc(var(--spacing)*12)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.text-blue-500{color:var(--color-blue-500)}.text-blue-600{color:var(--color-blue-600)}.text-card-foreground{color:var(--card-fg)}.text-destructive{color:var(--destructive)}.text-destructive-foreground{color:var(--destructive-fg)}.text-foreground,.text-foreground\/90{color:var(--fg)}@supports (color:color-mix(in lab, red, red)){.text-foreground\/90{color:color-mix(in oklab,var(--fg)90%,transparent)}}.text-gray-600{color:var(--color-gray-600)}.text-green-500{color:var(--color-green-500)}.text-green-600{color:var(--color-green-600)}.text-muted-foreground,.text-muted-foreground\/30{color:var(--muted-fg)}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/30{color:color-mix(in oklab,var(--muted-fg)30%,transparent)}}.text-muted-foreground\/40{color:var(--muted-fg)}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/40{color:color-mix(in oklab,var(--muted-fg)40%,transparent)}}.text-muted-foreground\/50{color:var(--muted-fg)}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/50{color:color-mix(in oklab,var(--muted-fg)50%,transparent)}}.text-muted-foreground\/60{color:var(--muted-fg)}@supports (color:color-mix(in lab, red, red)){.text-muted-foreground\/60{color:color-mix(in oklab,var(--muted-fg)60%,transparent)}}.text-orange-600{color:var(--color-orange-600)}.text-primary{color:var(--primary)}.text-primary-foreground{color:var(--primary-fg)}.text-purple-600{color:var(--color-purple-600)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-secondary-foreground{color:var(--secondary-fg)}.text-teal-600{color:var(--color-teal-600)}.text-yellow-500{color:var(--color-yellow-500)}.text-yellow-500\/60{color:#edb20099}@supports (color:color-mix(in lab, red, red)){.text-yellow-500\/60{color:color-mix(in oklab,var(--color-yellow-500)60%,transparent)}}.text-yellow-600{color:var(--color-yellow-600)}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,)var(--tw-slashed-zero,)var(--tw-numeric-figure,)var(--tw-numeric-spacing,)var(--tw-numeric-fraction,)}.underline{text-decoration-line:underline}.underline-offset-4{text-underline-offset:4px}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-blue-500\/40{--tw-ring-color:#3080ff66}@supports (color:color-mix(in lab, red, red)){.ring-blue-500\/40{--tw-ring-color:color-mix(in oklab,var(--color-blue-500)40%,transparent)}}.ring-offset-background{--tw-ring-offset-color:var(--bg)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.transition-\[max-height\,opacity\]{transition-property:max-height,opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){.group-hover\:text-foreground:is(:where(.group):hover *){color:var(--fg)}}.placeholder\:text-muted-foreground::placeholder{color:var(--muted-fg)}@media (hover:hover){.hover\:bg-accent:hover,.hover\:bg-accent\/30:hover{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-accent\/30:hover{background-color:color-mix(in oklab,var(--accent)30%,transparent)}}.hover\:bg-accent\/50:hover{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-accent\/50:hover{background-color:color-mix(in oklab,var(--accent)50%,transparent)}}.hover\:bg-blue-500\/10:hover{background-color:#3080ff1a}@supports (color:color-mix(in lab, red, red)){.hover\:bg-blue-500\/10:hover{background-color:color-mix(in oklab,var(--color-blue-500)10%,transparent)}}.hover\:bg-blue-500\/20:hover{background-color:#3080ff33}@supports (color:color-mix(in lab, red, red)){.hover\:bg-blue-500\/20:hover{background-color:color-mix(in oklab,var(--color-blue-500)20%,transparent)}}.hover\:bg-blue-500\/30:hover{background-color:#3080ff4d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-blue-500\/30:hover{background-color:color-mix(in oklab,var(--color-blue-500)30%,transparent)}}.hover\:bg-destructive\/90:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-destructive\/90:hover{background-color:color-mix(in oklab,var(--destructive)90%,transparent)}}.hover\:bg-muted\/80:hover{background-color:var(--muted)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-muted\/80:hover{background-color:color-mix(in oklab,var(--muted)80%,transparent)}}.hover\:bg-primary\/90:hover{background-color:var(--primary)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab,var(--primary)90%,transparent)}}.hover\:bg-secondary\/80:hover{background-color:var(--secondary)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-secondary\/80:hover{background-color:color-mix(in oklab,var(--secondary)80%,transparent)}}.hover\:text-accent-foreground:hover{color:var(--accent-fg)}.hover\:text-foreground:hover{color:var(--fg)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-80:hover{opacity:.8}}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-ring:focus{--tw-ring-color:var(--ring)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:ring-1:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color:var(--ring)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.active\:bg-blue-500\/50:active{background-color:#3080ff80}@supports (color:color-mix(in lab, red, red)){.active\:bg-blue-500\/50:active{background-color:color-mix(in oklab,var(--color-blue-500)50%,transparent)}}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-50:disabled{opacity:.5}.data-\[state\=active\]\:bg-background[data-state=active]{background-color:var(--bg)}.data-\[state\=active\]\:text-foreground[data-state=active]{color:var(--fg)}.data-\[state\=active\]\:shadow[data-state=active]{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:text-blue-300:is(.dark *){color:var(--color-blue-300)}.dark\:text-blue-400:is(.dark *){color:var(--color-blue-400)}.dark\:text-gray-400:is(.dark *){color:var(--color-gray-400)}.dark\:text-green-400:is(.dark *){color:var(--color-green-400)}.dark\:text-orange-400:is(.dark *){color:var(--color-orange-400)}.dark\:text-purple-400:is(.dark *){color:var(--color-purple-400)}.dark\:text-red-300:is(.dark *){color:var(--color-red-300)}.dark\:text-red-400:is(.dark *){color:var(--color-red-400)}.dark\:text-teal-400:is(.dark *){color:var(--color-teal-400)}.dark\:text-yellow-400:is(.dark *){color:var(--color-yellow-400)}}:root{--bg:#fff;--fg:#0a0a0a;--card:#fff;--card-fg:#0a0a0a;--popover:#fff;--popover-fg:#0a0a0a;--primary:#171717;--primary-fg:#fafafa;--secondary:#f5f5f5;--secondary-fg:#171717;--muted:#f5f5f5;--muted-fg:#737373;--accent:#f5f5f5;--accent-fg:#171717;--destructive:#ef4444;--destructive-fg:#fafafa;--border:#e5e5e5;--input:#e5e5e5;--ring:#0a0a0a}.dark{--bg:#0a0a0a;--fg:#fafafa;--card:#0a0a0a;--card-fg:#fafafa;--popover:#0a0a0a;--popover-fg:#fafafa;--primary:#fafafa;--primary-fg:#171717;--secondary:#262626;--secondary-fg:#fafafa;--muted:#262626;--muted-fg:#a3a3a3;--accent:#262626;--accent-fg:#fafafa;--destructive:#7f1d1d;--destructive-fg:#fafafa;--border:#262626;--input:#262626;--ring:#d4d4d4}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}
|