mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 22:32:27 +08:00
feat: link user feedback and diagram saves to chat traces in Langfuse
- Update log-feedback API to find existing chat trace by sessionId and attach score to it - Update log-save API to create span on existing chat trace instead of standalone trace - Add thumbs up/down feedback buttons on assistant messages - Add message regeneration and edit functionality - Add save dialog with format selection (drawio, png, svg) - Pass sessionId through components for Langfuse linking
This commit is contained in:
95
app/api/log-feedback/route.ts
Normal file
95
app/api/log-feedback/route.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { LangfuseClient } from '@langfuse/client';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
// Check if Langfuse is configured
|
||||||
|
if (!process.env.LANGFUSE_PUBLIC_KEY || !process.env.LANGFUSE_SECRET_KEY) {
|
||||||
|
return Response.json({ success: true, logged: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { messageId, feedback, sessionId } = await req.json();
|
||||||
|
|
||||||
|
// Get user IP for tracking
|
||||||
|
const forwardedFor = req.headers.get('x-forwarded-for');
|
||||||
|
const userId = forwardedFor?.split(',')[0]?.trim() || 'anonymous';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create Langfuse client
|
||||||
|
const langfuse = new LangfuseClient({
|
||||||
|
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
|
||||||
|
secretKey: process.env.LANGFUSE_SECRET_KEY,
|
||||||
|
baseUrl: process.env.LANGFUSE_BASEURL,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find the most recent chat trace for this session to attach the score to
|
||||||
|
const tracesResponse = await langfuse.api.trace.list({
|
||||||
|
sessionId,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const traces = tracesResponse.data || [];
|
||||||
|
const latestTrace = traces[0];
|
||||||
|
|
||||||
|
if (!latestTrace) {
|
||||||
|
// No trace found for this session - create a standalone feedback trace
|
||||||
|
const traceId = randomUUID();
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
|
||||||
|
await langfuse.api.ingestion.batch({
|
||||||
|
batch: [
|
||||||
|
{
|
||||||
|
type: 'trace-create',
|
||||||
|
id: randomUUID(),
|
||||||
|
timestamp,
|
||||||
|
body: {
|
||||||
|
id: traceId,
|
||||||
|
name: 'user-feedback',
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
input: { messageId, feedback },
|
||||||
|
metadata: { source: 'feedback-button', note: 'standalone - no chat trace found' },
|
||||||
|
timestamp,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'score-create',
|
||||||
|
id: randomUUID(),
|
||||||
|
timestamp,
|
||||||
|
body: {
|
||||||
|
id: randomUUID(),
|
||||||
|
traceId,
|
||||||
|
name: 'user-feedback',
|
||||||
|
value: feedback === 'good' ? 1 : 0,
|
||||||
|
comment: `User gave ${feedback} feedback`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Attach score to the existing chat trace
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
|
||||||
|
await langfuse.api.ingestion.batch({
|
||||||
|
batch: [
|
||||||
|
{
|
||||||
|
type: 'score-create',
|
||||||
|
id: randomUUID(),
|
||||||
|
timestamp,
|
||||||
|
body: {
|
||||||
|
id: randomUUID(),
|
||||||
|
traceId: latestTrace.id,
|
||||||
|
name: 'user-feedback',
|
||||||
|
value: feedback === 'good' ? 1 : 0,
|
||||||
|
comment: `User gave ${feedback} feedback`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ success: true, logged: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Langfuse feedback error:', error);
|
||||||
|
return Response.json({ success: false, error: String(error) }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
86
app/api/log-save/route.ts
Normal file
86
app/api/log-save/route.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { LangfuseClient } from '@langfuse/client';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
// Check if Langfuse is configured
|
||||||
|
if (!process.env.LANGFUSE_PUBLIC_KEY || !process.env.LANGFUSE_SECRET_KEY) {
|
||||||
|
return Response.json({ success: true, logged: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { xml, filename, format, sessionId } = await req.json();
|
||||||
|
|
||||||
|
// Get user IP for tracking
|
||||||
|
const forwardedFor = req.headers.get('x-forwarded-for');
|
||||||
|
const userId = forwardedFor?.split(',')[0]?.trim() || 'anonymous';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create Langfuse client
|
||||||
|
const langfuse = new LangfuseClient({
|
||||||
|
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
|
||||||
|
secretKey: process.env.LANGFUSE_SECRET_KEY,
|
||||||
|
baseUrl: process.env.LANGFUSE_BASEURL,
|
||||||
|
});
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
|
||||||
|
// Find the most recent chat trace for this session to attach the save event to
|
||||||
|
const tracesResponse = await langfuse.api.trace.list({
|
||||||
|
sessionId,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const traces = tracesResponse.data || [];
|
||||||
|
const latestTrace = traces[0];
|
||||||
|
|
||||||
|
if (latestTrace) {
|
||||||
|
// Create a span on the existing chat trace for the save event
|
||||||
|
await langfuse.api.ingestion.batch({
|
||||||
|
batch: [
|
||||||
|
{
|
||||||
|
type: 'span-create',
|
||||||
|
id: randomUUID(),
|
||||||
|
timestamp,
|
||||||
|
body: {
|
||||||
|
id: randomUUID(),
|
||||||
|
traceId: latestTrace.id,
|
||||||
|
name: 'diagram-save',
|
||||||
|
input: { filename, format },
|
||||||
|
output: { xmlPreview: xml?.substring(0, 500), contentLength: xml?.length || 0 },
|
||||||
|
metadata: { source: 'save-button' },
|
||||||
|
startTime: timestamp,
|
||||||
|
endTime: timestamp,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// No trace found - create a standalone trace
|
||||||
|
const traceId = randomUUID();
|
||||||
|
|
||||||
|
await langfuse.api.ingestion.batch({
|
||||||
|
batch: [
|
||||||
|
{
|
||||||
|
type: 'trace-create',
|
||||||
|
id: randomUUID(),
|
||||||
|
timestamp,
|
||||||
|
body: {
|
||||||
|
id: traceId,
|
||||||
|
name: 'diagram-save',
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
input: { filename, format },
|
||||||
|
output: { xmlPreview: xml?.substring(0, 500), contentLength: xml?.length || 0 },
|
||||||
|
metadata: { source: 'save-button', note: 'standalone - no chat trace found' },
|
||||||
|
timestamp,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ success: true, logged: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Langfuse save error:', error);
|
||||||
|
return Response.json({ success: false, error: String(error) }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ interface ChatInputProps {
|
|||||||
onFileChange?: (files: File[]) => void;
|
onFileChange?: (files: File[]) => void;
|
||||||
showHistory?: boolean;
|
showHistory?: boolean;
|
||||||
onToggleHistory?: (show: boolean) => void;
|
onToggleHistory?: (show: boolean) => void;
|
||||||
|
sessionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatInput({
|
export function ChatInput({
|
||||||
@@ -41,6 +42,7 @@ export function ChatInput({
|
|||||||
onFileChange = () => {},
|
onFileChange = () => {},
|
||||||
showHistory = false,
|
showHistory = false,
|
||||||
onToggleHistory = () => {},
|
onToggleHistory = () => {},
|
||||||
|
sessionId,
|
||||||
}: ChatInputProps) {
|
}: ChatInputProps) {
|
||||||
const { diagramHistory, saveDiagramToFile } = useDiagram();
|
const { diagramHistory, saveDiagramToFile } = useDiagram();
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
@@ -249,7 +251,7 @@ export function ChatInput({
|
|||||||
<SaveDialog
|
<SaveDialog
|
||||||
open={showSaveDialog}
|
open={showSaveDialog}
|
||||||
onOpenChange={setShowSaveDialog}
|
onOpenChange={setShowSaveDialog}
|
||||||
onSave={saveDiagramToFile}
|
onSave={(filename, format) => saveDiagramToFile(filename, format, sessionId)}
|
||||||
defaultFilename={`diagram-${new Date().toISOString().slice(0, 10)}`}
|
defaultFilename={`diagram-${new Date().toISOString().slice(0, 10)}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
|||||||
import ExamplePanel from "./chat-example-panel";
|
import ExamplePanel from "./chat-example-panel";
|
||||||
import { UIMessage } from "ai";
|
import { UIMessage } from "ai";
|
||||||
import { convertToLegalXml, replaceNodes, validateMxCellStructure } from "@/lib/utils";
|
import { convertToLegalXml, replaceNodes, validateMxCellStructure } from "@/lib/utils";
|
||||||
import { Copy, Check, X, ChevronDown, ChevronUp, Cpu, Minus, Plus } from "lucide-react";
|
import { Copy, Check, X, ChevronDown, ChevronUp, Cpu, Minus, Plus, ThumbsUp, ThumbsDown, RotateCcw, Pencil } from "lucide-react";
|
||||||
import { CodeBlock } from "./code-block";
|
import { CodeBlock } from "./code-block";
|
||||||
|
|
||||||
interface EditPair {
|
interface EditPair {
|
||||||
@@ -67,6 +67,9 @@ interface ChatMessageDisplayProps {
|
|||||||
error?: Error | null;
|
error?: Error | null;
|
||||||
setInput: (input: string) => void;
|
setInput: (input: string) => void;
|
||||||
setFiles: (files: File[]) => void;
|
setFiles: (files: File[]) => void;
|
||||||
|
sessionId?: string;
|
||||||
|
onRegenerate?: (messageIndex: number) => void;
|
||||||
|
onEditMessage?: (messageIndex: number, newText: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatMessageDisplay({
|
export function ChatMessageDisplay({
|
||||||
@@ -74,6 +77,9 @@ export function ChatMessageDisplay({
|
|||||||
error,
|
error,
|
||||||
setInput,
|
setInput,
|
||||||
setFiles,
|
setFiles,
|
||||||
|
sessionId,
|
||||||
|
onRegenerate,
|
||||||
|
onEditMessage,
|
||||||
}: ChatMessageDisplayProps) {
|
}: ChatMessageDisplayProps) {
|
||||||
const { chartXML, loadDiagram: onDisplayChart } = useDiagram();
|
const { chartXML, loadDiagram: onDisplayChart } = useDiagram();
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -84,6 +90,9 @@ export function ChatMessageDisplay({
|
|||||||
);
|
);
|
||||||
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
|
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
|
||||||
const [copyFailedMessageId, setCopyFailedMessageId] = useState<string | null>(null);
|
const [copyFailedMessageId, setCopyFailedMessageId] = useState<string | null>(null);
|
||||||
|
const [feedback, setFeedback] = useState<Record<string, "good" | "bad">>({});
|
||||||
|
const [editingMessageId, setEditingMessageId] = useState<string | null>(null);
|
||||||
|
const [editText, setEditText] = useState<string>("");
|
||||||
|
|
||||||
const copyMessageToClipboard = async (messageId: string, text: string) => {
|
const copyMessageToClipboard = async (messageId: string, text: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -97,6 +106,34 @@ export function ChatMessageDisplay({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submitFeedback = async (messageId: string, value: "good" | "bad") => {
|
||||||
|
// Toggle off if already selected
|
||||||
|
if (feedback[messageId] === value) {
|
||||||
|
setFeedback((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[messageId];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFeedback((prev) => ({ ...prev, [messageId]: value }));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch("/api/log-feedback", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
messageId,
|
||||||
|
feedback: value,
|
||||||
|
sessionId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to log feedback:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDisplayChart = useCallback(
|
const handleDisplayChart = useCallback(
|
||||||
(xml: string) => {
|
(xml: string) => {
|
||||||
const currentXml = xml || "";
|
const currentXml = xml || "";
|
||||||
@@ -253,65 +290,146 @@ export function ChatMessageDisplay({
|
|||||||
<div className="py-4 space-y-4">
|
<div className="py-4 space-y-4">
|
||||||
{messages.map((message, messageIndex) => {
|
{messages.map((message, messageIndex) => {
|
||||||
const userMessageText = message.role === "user" ? getMessageTextContent(message) : "";
|
const userMessageText = message.role === "user" ? getMessageTextContent(message) : "";
|
||||||
|
const isLastAssistantMessage = message.role === "assistant" && (
|
||||||
|
messageIndex === messages.length - 1 ||
|
||||||
|
messages.slice(messageIndex + 1).every(m => m.role !== "assistant")
|
||||||
|
);
|
||||||
|
const isLastUserMessage = message.role === "user" && (
|
||||||
|
messageIndex === messages.length - 1 ||
|
||||||
|
messages.slice(messageIndex + 1).every(m => m.role !== "user")
|
||||||
|
);
|
||||||
|
const isEditing = editingMessageId === message.id;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={message.id}
|
key={message.id}
|
||||||
className={`flex ${message.role === "user" ? "justify-end" : "justify-start"} animate-message-in`}
|
className={`flex ${message.role === "user" ? "justify-end" : "justify-start"} animate-message-in`}
|
||||||
style={{ animationDelay: `${messageIndex * 50}ms` }}
|
style={{ animationDelay: `${messageIndex * 50}ms` }}
|
||||||
>
|
>
|
||||||
{message.role === "user" && userMessageText && (
|
{message.role === "user" && userMessageText && !isEditing && (
|
||||||
<button
|
<div className="flex items-center gap-1 self-center mr-2">
|
||||||
onClick={() => copyMessageToClipboard(message.id, userMessageText)}
|
{/* Edit button - only on last user message */}
|
||||||
className="p-1.5 rounded-lg text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted transition-colors self-center mr-2"
|
{onEditMessage && isLastUserMessage && (
|
||||||
title={copiedMessageId === message.id ? "Copied!" : copyFailedMessageId === message.id ? "Failed to copy" : "Copy message"}
|
<button
|
||||||
>
|
onClick={() => {
|
||||||
{copiedMessageId === message.id ? (
|
setEditingMessageId(message.id);
|
||||||
<Check className="h-3.5 w-3.5 text-green-500" />
|
setEditText(userMessageText);
|
||||||
) : copyFailedMessageId === message.id ? (
|
}}
|
||||||
<X className="h-3.5 w-3.5 text-red-500" />
|
className="p-1.5 rounded-lg text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted transition-colors"
|
||||||
) : (
|
title="Edit message"
|
||||||
<Copy className="h-3.5 w-3.5" />
|
>
|
||||||
|
<Pencil className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</button>
|
<button
|
||||||
|
onClick={() => copyMessageToClipboard(message.id, userMessageText)}
|
||||||
|
className="p-1.5 rounded-lg text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted transition-colors"
|
||||||
|
title={copiedMessageId === message.id ? "Copied!" : copyFailedMessageId === message.id ? "Failed to copy" : "Copy message"}
|
||||||
|
>
|
||||||
|
{copiedMessageId === message.id ? (
|
||||||
|
<Check className="h-3.5 w-3.5 text-green-500" />
|
||||||
|
) : copyFailedMessageId === message.id ? (
|
||||||
|
<X className="h-3.5 w-3.5 text-red-500" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="max-w-[85%]">
|
<div className="max-w-[85%]">
|
||||||
{/* Text content in bubble */}
|
{/* Edit mode for user messages */}
|
||||||
{message.parts?.some((part: any) => part.type === "text" || part.type === "file") && (
|
{isEditing && message.role === "user" ? (
|
||||||
<div
|
<div className="flex flex-col gap-2">
|
||||||
className={`px-4 py-3 text-sm leading-relaxed ${
|
<textarea
|
||||||
message.role === "user"
|
value={editText}
|
||||||
? "bg-primary text-primary-foreground rounded-2xl rounded-br-md shadow-sm"
|
onChange={(e) => setEditText(e.target.value)}
|
||||||
: "bg-muted/60 text-foreground rounded-2xl rounded-bl-md"
|
className="w-full min-w-[300px] px-4 py-3 text-sm rounded-2xl border border-primary bg-background text-foreground resize-none focus:outline-none focus:ring-2 focus:ring-primary"
|
||||||
}`}
|
rows={Math.min(editText.split('\n').length + 1, 6)}
|
||||||
>
|
autoFocus
|
||||||
{message.parts?.map((part: any, index: number) => {
|
onKeyDown={(e) => {
|
||||||
switch (part.type) {
|
if (e.key === "Escape") {
|
||||||
case "text":
|
setEditingMessageId(null);
|
||||||
return (
|
setEditText("");
|
||||||
<div key={index} className="whitespace-pre-wrap break-words">
|
} else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
||||||
{part.text}
|
e.preventDefault();
|
||||||
</div>
|
if (editText.trim() && onEditMessage) {
|
||||||
);
|
onEditMessage(messageIndex, editText.trim());
|
||||||
case "file":
|
setEditingMessageId(null);
|
||||||
return (
|
setEditText("");
|
||||||
<div key={index} className="mt-2">
|
}
|
||||||
<Image
|
}
|
||||||
src={part.url}
|
}}
|
||||||
width={200}
|
/>
|
||||||
height={200}
|
<div className="flex justify-end gap-2">
|
||||||
alt={`Uploaded diagram or image for AI analysis`}
|
<button
|
||||||
className="rounded-lg border border-white/20"
|
onClick={() => {
|
||||||
style={{
|
setEditingMessageId(null);
|
||||||
objectFit: "contain",
|
setEditText("");
|
||||||
}}
|
}}
|
||||||
/>
|
className="px-3 py-1.5 text-xs rounded-lg bg-muted hover:bg-muted/80 transition-colors"
|
||||||
</div>
|
>
|
||||||
);
|
Cancel
|
||||||
default:
|
</button>
|
||||||
return null;
|
<button
|
||||||
}
|
onClick={() => {
|
||||||
})}
|
if (editText.trim() && onEditMessage) {
|
||||||
|
onEditMessage(messageIndex, editText.trim());
|
||||||
|
setEditingMessageId(null);
|
||||||
|
setEditText("");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!editText.trim()}
|
||||||
|
className="px-3 py-1.5 text-xs rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 transition-colors"
|
||||||
|
>
|
||||||
|
Save & Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
/* Text content in bubble */
|
||||||
|
message.parts?.some((part: any) => part.type === "text" || part.type === "file") && (
|
||||||
|
<div
|
||||||
|
className={`px-4 py-3 text-sm leading-relaxed ${
|
||||||
|
message.role === "user"
|
||||||
|
? "bg-primary text-primary-foreground rounded-2xl rounded-br-md shadow-sm"
|
||||||
|
: "bg-muted/60 text-foreground rounded-2xl rounded-bl-md"
|
||||||
|
} ${message.role === "user" && isLastUserMessage && onEditMessage ? "cursor-pointer hover:opacity-90 transition-opacity" : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (message.role === "user" && isLastUserMessage && onEditMessage) {
|
||||||
|
setEditingMessageId(message.id);
|
||||||
|
setEditText(userMessageText);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title={message.role === "user" && isLastUserMessage && onEditMessage ? "Click to edit" : undefined}
|
||||||
|
>
|
||||||
|
{message.parts?.map((part: any, index: number) => {
|
||||||
|
switch (part.type) {
|
||||||
|
case "text":
|
||||||
|
return (
|
||||||
|
<div key={index} className="whitespace-pre-wrap break-words">
|
||||||
|
{part.text}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "file":
|
||||||
|
return (
|
||||||
|
<div key={index} className="mt-2">
|
||||||
|
<Image
|
||||||
|
src={part.url}
|
||||||
|
width={200}
|
||||||
|
height={200}
|
||||||
|
alt={`Uploaded diagram or image for AI analysis`}
|
||||||
|
className="rounded-lg border border-white/20"
|
||||||
|
style={{
|
||||||
|
objectFit: "contain",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
{/* Tool calls outside bubble */}
|
{/* Tool calls outside bubble */}
|
||||||
{message.parts?.map((part: any) => {
|
{message.parts?.map((part: any) => {
|
||||||
@@ -320,6 +438,63 @@ export function ChatMessageDisplay({
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
})}
|
})}
|
||||||
|
{/* Action buttons for assistant messages */}
|
||||||
|
{message.role === "assistant" && (
|
||||||
|
<div className="flex items-center gap-1 mt-2">
|
||||||
|
{/* Copy button */}
|
||||||
|
<button
|
||||||
|
onClick={() => copyMessageToClipboard(message.id, getMessageTextContent(message))}
|
||||||
|
className={`p-1.5 rounded-lg transition-colors ${
|
||||||
|
copiedMessageId === message.id
|
||||||
|
? "text-green-600 bg-green-100"
|
||||||
|
: "text-muted-foreground/60 hover:text-foreground hover:bg-muted"
|
||||||
|
}`}
|
||||||
|
title={copiedMessageId === message.id ? "Copied!" : "Copy response"}
|
||||||
|
>
|
||||||
|
{copiedMessageId === message.id ? (
|
||||||
|
<Check className="h-3.5 w-3.5" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{/* Regenerate button - only on last assistant message */}
|
||||||
|
{onRegenerate && isLastAssistantMessage && (
|
||||||
|
<button
|
||||||
|
onClick={() => onRegenerate(messageIndex)}
|
||||||
|
className="p-1.5 rounded-lg text-muted-foreground/60 hover:text-foreground hover:bg-muted transition-colors"
|
||||||
|
title="Regenerate response"
|
||||||
|
>
|
||||||
|
<RotateCcw className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="w-px h-4 bg-border mx-1" />
|
||||||
|
{/* Thumbs up */}
|
||||||
|
<button
|
||||||
|
onClick={() => submitFeedback(message.id, "good")}
|
||||||
|
className={`p-1.5 rounded-lg transition-colors ${
|
||||||
|
feedback[message.id] === "good"
|
||||||
|
? "text-green-600 bg-green-100"
|
||||||
|
: "text-muted-foreground/60 hover:text-green-600 hover:bg-green-50"
|
||||||
|
}`}
|
||||||
|
title="Good response"
|
||||||
|
>
|
||||||
|
<ThumbsUp className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
{/* Thumbs down */}
|
||||||
|
<button
|
||||||
|
onClick={() => submitFeedback(message.id, "bad")}
|
||||||
|
className={`p-1.5 rounded-lg transition-colors ${
|
||||||
|
feedback[message.id] === "bad"
|
||||||
|
? "text-red-600 bg-red-100"
|
||||||
|
: "text-muted-foreground/60 hover:text-red-600 hover:bg-red-50"
|
||||||
|
}`}
|
||||||
|
title="Bad response"
|
||||||
|
>
|
||||||
|
<ThumbsDown className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useRef, useEffect, useState } from "react";
|
import { useRef, useEffect, useState } from "react";
|
||||||
|
import { flushSync } from "react-dom";
|
||||||
import { FaGithub } from "react-icons/fa";
|
import { FaGithub } from "react-icons/fa";
|
||||||
import { PanelRightClose, PanelRightOpen } from "lucide-react";
|
import { PanelRightClose, PanelRightOpen } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -64,6 +65,9 @@ export default function ChatPanel({
|
|||||||
// Generate a unique session ID for Langfuse tracing
|
// Generate a unique session ID for Langfuse tracing
|
||||||
const [sessionId, setSessionId] = useState(() => `session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`);
|
const [sessionId, setSessionId] = useState(() => `session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`);
|
||||||
|
|
||||||
|
// Store XML snapshots for each user message (keyed by message index)
|
||||||
|
const xmlSnapshotsRef = useRef<Map<number, string>>(new Map());
|
||||||
|
|
||||||
const { messages, sendMessage, addToolResult, status, error, setMessages } =
|
const { messages, sendMessage, addToolResult, status, error, setMessages } =
|
||||||
useChat({
|
useChat({
|
||||||
transport: new DefaultChatTransport({
|
transport: new DefaultChatTransport({
|
||||||
@@ -172,6 +176,10 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save XML snapshot for this message (will be at index = current messages.length)
|
||||||
|
const messageIndex = messages.length;
|
||||||
|
xmlSnapshotsRef.current.set(messageIndex, chartXml);
|
||||||
|
|
||||||
sendMessage(
|
sendMessage(
|
||||||
{ parts },
|
{ parts },
|
||||||
{
|
{
|
||||||
@@ -200,6 +208,112 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
setFiles(newFiles);
|
setFiles(newFiles);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRegenerate = async (messageIndex: number) => {
|
||||||
|
const isProcessing = status === "streaming" || status === "submitted";
|
||||||
|
if (isProcessing) return;
|
||||||
|
|
||||||
|
// Find the user message before this assistant message
|
||||||
|
let userMessageIndex = messageIndex - 1;
|
||||||
|
while (userMessageIndex >= 0 && messages[userMessageIndex].role !== "user") {
|
||||||
|
userMessageIndex--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userMessageIndex < 0) return;
|
||||||
|
|
||||||
|
const userMessage = messages[userMessageIndex];
|
||||||
|
const userParts = userMessage.parts;
|
||||||
|
|
||||||
|
// Get the text from the user message
|
||||||
|
const textPart = userParts?.find((p: any) => p.type === "text");
|
||||||
|
if (!textPart) return;
|
||||||
|
|
||||||
|
// Get the saved XML snapshot for this user message
|
||||||
|
const savedXml = xmlSnapshotsRef.current.get(userMessageIndex);
|
||||||
|
if (!savedXml) {
|
||||||
|
console.error("No saved XML snapshot for message index:", userMessageIndex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore the diagram to the saved state
|
||||||
|
onDisplayChart(savedXml);
|
||||||
|
|
||||||
|
// Clean up snapshots for messages after the user message (they will be removed)
|
||||||
|
for (const key of xmlSnapshotsRef.current.keys()) {
|
||||||
|
if (key > userMessageIndex) {
|
||||||
|
xmlSnapshotsRef.current.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the user message AND assistant message onwards (sendMessage will re-add the user message)
|
||||||
|
// Use flushSync to ensure state update is processed synchronously before sending
|
||||||
|
const newMessages = messages.slice(0, userMessageIndex);
|
||||||
|
flushSync(() => {
|
||||||
|
setMessages(newMessages);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now send the message after state is guaranteed to be updated
|
||||||
|
sendMessage(
|
||||||
|
{ parts: userParts },
|
||||||
|
{
|
||||||
|
body: {
|
||||||
|
xml: savedXml,
|
||||||
|
sessionId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditMessage = async (messageIndex: number, newText: string) => {
|
||||||
|
const isProcessing = status === "streaming" || status === "submitted";
|
||||||
|
if (isProcessing) return;
|
||||||
|
|
||||||
|
const message = messages[messageIndex];
|
||||||
|
if (!message || message.role !== "user") return;
|
||||||
|
|
||||||
|
// Get the saved XML snapshot for this user message
|
||||||
|
const savedXml = xmlSnapshotsRef.current.get(messageIndex);
|
||||||
|
if (!savedXml) {
|
||||||
|
console.error("No saved XML snapshot for message index:", messageIndex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore the diagram to the saved state
|
||||||
|
onDisplayChart(savedXml);
|
||||||
|
|
||||||
|
// Clean up snapshots for messages after the user message (they will be removed)
|
||||||
|
for (const key of xmlSnapshotsRef.current.keys()) {
|
||||||
|
if (key > messageIndex) {
|
||||||
|
xmlSnapshotsRef.current.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new parts with updated text
|
||||||
|
const newParts = message.parts?.map((part: any) => {
|
||||||
|
if (part.type === "text") {
|
||||||
|
return { ...part, text: newText };
|
||||||
|
}
|
||||||
|
return part;
|
||||||
|
}) || [{ type: "text", text: newText }];
|
||||||
|
|
||||||
|
// Remove the user message AND assistant message onwards (sendMessage will re-add the user message)
|
||||||
|
// Use flushSync to ensure state update is processed synchronously before sending
|
||||||
|
const newMessages = messages.slice(0, messageIndex);
|
||||||
|
flushSync(() => {
|
||||||
|
setMessages(newMessages);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now send the edited message after state is guaranteed to be updated
|
||||||
|
sendMessage(
|
||||||
|
{ parts: newParts },
|
||||||
|
{
|
||||||
|
body: {
|
||||||
|
xml: savedXml,
|
||||||
|
sessionId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Collapsed view
|
// Collapsed view
|
||||||
if (!isVisible) {
|
if (!isVisible) {
|
||||||
return (
|
return (
|
||||||
@@ -281,6 +395,9 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
error={error}
|
error={error}
|
||||||
setInput={setInput}
|
setInput={setInput}
|
||||||
setFiles={handleFileChange}
|
setFiles={handleFileChange}
|
||||||
|
sessionId={sessionId}
|
||||||
|
onRegenerate={handleRegenerate}
|
||||||
|
onEditMessage={handleEditMessage}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -295,11 +412,13 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
setMessages([]);
|
setMessages([]);
|
||||||
clearDiagram();
|
clearDiagram();
|
||||||
setSessionId(`session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`);
|
setSessionId(`session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`);
|
||||||
|
xmlSnapshotsRef.current.clear();
|
||||||
}}
|
}}
|
||||||
files={files}
|
files={files}
|
||||||
onFileChange={handleFileChange}
|
onFileChange={handleFileChange}
|
||||||
showHistory={showHistory}
|
showHistory={showHistory}
|
||||||
onToggleHistory={setShowHistory}
|
onToggleHistory={setShowHistory}
|
||||||
|
sessionId={sessionId}
|
||||||
/>
|
/>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,11 +10,26 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
|
export type ExportFormat = "drawio" | "png" | "svg";
|
||||||
|
|
||||||
|
const FORMAT_OPTIONS: { value: ExportFormat; label: string; extension: string }[] = [
|
||||||
|
{ value: "drawio", label: "Draw.io XML", extension: ".drawio" },
|
||||||
|
{ value: "png", label: "PNG Image", extension: ".png" },
|
||||||
|
{ value: "svg", label: "SVG Image", extension: ".svg" },
|
||||||
|
];
|
||||||
|
|
||||||
interface SaveDialogProps {
|
interface SaveDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
onSave: (filename: string) => void;
|
onSave: (filename: string, format: ExportFormat) => void;
|
||||||
defaultFilename: string;
|
defaultFilename: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,6 +40,7 @@ export function SaveDialog({
|
|||||||
defaultFilename,
|
defaultFilename,
|
||||||
}: SaveDialogProps) {
|
}: SaveDialogProps) {
|
||||||
const [filename, setFilename] = useState(defaultFilename);
|
const [filename, setFilename] = useState(defaultFilename);
|
||||||
|
const [format, setFormat] = useState<ExportFormat>("drawio");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
@@ -34,7 +50,7 @@ export function SaveDialog({
|
|||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
const finalFilename = filename.trim() || defaultFilename;
|
const finalFilename = filename.trim() || defaultFilename;
|
||||||
onSave(finalFilename);
|
onSave(finalFilename, format);
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,27 +61,46 @@ export function SaveDialog({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const currentFormat = FORMAT_OPTIONS.find((f) => f.value === format);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Save Diagram</DialogTitle>
|
<DialogTitle>Save Diagram</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-2">
|
<div className="space-y-4">
|
||||||
<label className="text-sm font-medium">Filename</label>
|
<div className="space-y-2">
|
||||||
<div className="flex items-stretch">
|
<label className="text-sm font-medium">Format</label>
|
||||||
<Input
|
<Select value={format} onValueChange={(v) => setFormat(v as ExportFormat)}>
|
||||||
value={filename}
|
<SelectTrigger>
|
||||||
onChange={(e) => setFilename(e.target.value)}
|
<SelectValue />
|
||||||
onKeyDown={handleKeyDown}
|
</SelectTrigger>
|
||||||
placeholder="Enter filename"
|
<SelectContent>
|
||||||
autoFocus
|
{FORMAT_OPTIONS.map((opt) => (
|
||||||
onFocus={(e) => e.target.select()}
|
<SelectItem key={opt.value} value={opt.value}>
|
||||||
className="rounded-r-none border-r-0 focus-visible:z-10"
|
{opt.label}
|
||||||
/>
|
</SelectItem>
|
||||||
<span className="inline-flex items-center px-3 rounded-r-md border border-l-0 border-input bg-muted text-sm text-muted-foreground font-mono">
|
))}
|
||||||
.drawio
|
</SelectContent>
|
||||||
</span>
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Filename</label>
|
||||||
|
<div className="flex items-stretch">
|
||||||
|
<Input
|
||||||
|
value={filename}
|
||||||
|
onChange={(e) => setFilename(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="Enter filename"
|
||||||
|
autoFocus
|
||||||
|
onFocus={(e) => e.target.select()}
|
||||||
|
className="rounded-r-none border-r-0 focus-visible:z-10"
|
||||||
|
/>
|
||||||
|
<span className="inline-flex items-center px-3 rounded-r-md border border-l-0 border-input bg-muted text-sm text-muted-foreground font-mono">
|
||||||
|
{currentFormat?.extension || ".drawio"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
|||||||
187
components/ui/select.tsx
Normal file
187
components/ui/select.tsx
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||||
|
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Select({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||||
|
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||||
|
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectValue({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||||
|
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectTrigger({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||||
|
size?: "sm" | "default"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
data-slot="select-trigger"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDownIcon className="size-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
position = "popper",
|
||||||
|
align = "center",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
data-slot="select-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||||
|
position === "popper" &&
|
||||||
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
align={align}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1",
|
||||||
|
position === "popper" &&
|
||||||
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
data-slot="select-label"
|
||||||
|
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
data-slot="select-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
data-slot="select-separator"
|
||||||
|
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollUpButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
data-slot="select-scroll-up-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUpIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollDownButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
data-slot="select-scroll-down-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import React, { createContext, useContext, useRef, useState } from "react";
|
import React, { createContext, useContext, useRef, useState } from "react";
|
||||||
import type { DrawIoEmbedRef } from "react-drawio";
|
import type { DrawIoEmbedRef } from "react-drawio";
|
||||||
import { extractDiagramXML } from "../lib/utils";
|
import { extractDiagramXML } from "../lib/utils";
|
||||||
|
import type { ExportFormat } from "@/components/save-dialog";
|
||||||
|
|
||||||
interface DiagramContextType {
|
interface DiagramContextType {
|
||||||
chartXML: string;
|
chartXML: string;
|
||||||
@@ -15,7 +16,7 @@ interface DiagramContextType {
|
|||||||
drawioRef: React.Ref<DrawIoEmbedRef | null>;
|
drawioRef: React.Ref<DrawIoEmbedRef | null>;
|
||||||
handleDiagramExport: (data: any) => void;
|
handleDiagramExport: (data: any) => void;
|
||||||
clearDiagram: () => void;
|
clearDiagram: () => void;
|
||||||
saveDiagramToFile: (filename: string) => void;
|
saveDiagramToFile: (filename: string, format: ExportFormat, sessionId?: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DiagramContext = createContext<DiagramContextType | undefined>(undefined);
|
const DiagramContext = createContext<DiagramContextType | undefined>(undefined);
|
||||||
@@ -30,8 +31,11 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const resolverRef = useRef<((value: string) => void) | null>(null);
|
const resolverRef = useRef<((value: string) => void) | null>(null);
|
||||||
// Track if we're expecting an export for history (user-initiated)
|
// Track if we're expecting an export for history (user-initiated)
|
||||||
const expectHistoryExportRef = useRef<boolean>(false);
|
const expectHistoryExportRef = useRef<boolean>(false);
|
||||||
// Track if we're expecting an export for file save
|
// Track if we're expecting an export for file save (stores raw export data)
|
||||||
const saveResolverRef = useRef<((xml: string) => void) | null>(null);
|
const saveResolverRef = useRef<{
|
||||||
|
resolver: ((data: string) => void) | null;
|
||||||
|
format: ExportFormat | null;
|
||||||
|
}>({ resolver: null, format: null });
|
||||||
|
|
||||||
const handleExport = () => {
|
const handleExport = () => {
|
||||||
if (drawioRef.current) {
|
if (drawioRef.current) {
|
||||||
@@ -61,6 +65,18 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDiagramExport = (data: any) => {
|
const handleDiagramExport = (data: any) => {
|
||||||
|
// Handle save to file if requested (process raw data before extraction)
|
||||||
|
if (saveResolverRef.current.resolver) {
|
||||||
|
const format = saveResolverRef.current.format;
|
||||||
|
saveResolverRef.current.resolver(data.data);
|
||||||
|
saveResolverRef.current = { resolver: null, format: null };
|
||||||
|
// For non-xmlsvg formats, skip XML extraction as it will fail
|
||||||
|
// Only drawio (which uses xmlsvg internally) has the content attribute
|
||||||
|
if (format === "png" || format === "svg") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const extractedXML = extractDiagramXML(data.data);
|
const extractedXML = extractDiagramXML(data.data);
|
||||||
setChartXML(extractedXML);
|
setChartXML(extractedXML);
|
||||||
setLatestSvg(data.data);
|
setLatestSvg(data.data);
|
||||||
@@ -81,12 +97,6 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
resolverRef.current(extractedXML);
|
resolverRef.current(extractedXML);
|
||||||
resolverRef.current = null;
|
resolverRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle save to file if requested
|
|
||||||
if (saveResolverRef.current) {
|
|
||||||
saveResolverRef.current(extractedXML);
|
|
||||||
saveResolverRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearDiagram = () => {
|
const clearDiagram = () => {
|
||||||
@@ -97,33 +107,89 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
setDiagramHistory([]);
|
setDiagramHistory([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveDiagramToFile = (filename: string) => {
|
const saveDiagramToFile = (filename: string, format: ExportFormat, sessionId?: string) => {
|
||||||
if (!drawioRef.current) {
|
if (!drawioRef.current) {
|
||||||
console.warn("Draw.io editor not ready");
|
console.warn("Draw.io editor not ready");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export diagram and save when export completes
|
// Map format to draw.io export format
|
||||||
drawioRef.current.exportDiagram({ format: "xmlsvg" });
|
const drawioFormat = format === "drawio" ? "xmlsvg" : format;
|
||||||
saveResolverRef.current = (xml: string) => {
|
|
||||||
// Wrap in proper .drawio format
|
|
||||||
let fileContent = xml;
|
|
||||||
if (!xml.includes("<mxfile")) {
|
|
||||||
fileContent = `<mxfile><diagram name="Page-1" id="page-1">${xml}</diagram></mxfile>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = new Blob([fileContent], { type: "application/xml" });
|
// Set up the resolver before triggering export
|
||||||
const url = URL.createObjectURL(blob);
|
saveResolverRef.current = {
|
||||||
const a = document.createElement("a");
|
resolver: (exportData: string) => {
|
||||||
a.href = url;
|
let fileContent: string | Blob;
|
||||||
// Add .drawio extension if not present
|
let mimeType: string;
|
||||||
a.download = filename.endsWith(".drawio") ? filename : `${filename}.drawio`;
|
let extension: string;
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
if (format === "drawio") {
|
||||||
document.body.removeChild(a);
|
// Extract XML from SVG for .drawio format
|
||||||
// Delay URL revocation to ensure download completes
|
const xml = extractDiagramXML(exportData);
|
||||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
let xmlContent = xml;
|
||||||
|
if (!xml.includes("<mxfile")) {
|
||||||
|
xmlContent = `<mxfile><diagram name="Page-1" id="page-1">${xml}</diagram></mxfile>`;
|
||||||
|
}
|
||||||
|
fileContent = xmlContent;
|
||||||
|
mimeType = "application/xml";
|
||||||
|
extension = ".drawio";
|
||||||
|
|
||||||
|
// Log XML to Langfuse
|
||||||
|
logSaveToLangfuse(xmlContent, filename, format, sessionId);
|
||||||
|
} else if (format === "png") {
|
||||||
|
// PNG data comes as base64 data URL
|
||||||
|
fileContent = exportData;
|
||||||
|
mimeType = "image/png";
|
||||||
|
extension = ".png";
|
||||||
|
logSaveToLangfuse(exportData, filename, format, sessionId);
|
||||||
|
} else {
|
||||||
|
// SVG format
|
||||||
|
fileContent = exportData;
|
||||||
|
mimeType = "image/svg+xml";
|
||||||
|
extension = ".svg";
|
||||||
|
logSaveToLangfuse(exportData, filename, format, sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle download
|
||||||
|
let url: string;
|
||||||
|
if (typeof fileContent === "string" && fileContent.startsWith("data:")) {
|
||||||
|
// Already a data URL (PNG)
|
||||||
|
url = fileContent;
|
||||||
|
} else {
|
||||||
|
const blob = new Blob([fileContent], { type: mimeType });
|
||||||
|
url = URL.createObjectURL(blob);
|
||||||
|
}
|
||||||
|
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${filename}${extension}`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
|
||||||
|
// Delay URL revocation to ensure download completes
|
||||||
|
if (!url.startsWith("data:")) {
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
format,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Export diagram - callback will be handled in handleDiagramExport
|
||||||
|
drawioRef.current.exportDiagram({ format: drawioFormat });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Log save event to Langfuse
|
||||||
|
const logSaveToLangfuse = async (content: string, filename: string, format: string, sessionId?: string) => {
|
||||||
|
try {
|
||||||
|
await fetch("/api/log-save", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ xml: content, filename, format, sessionId }),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to log save to Langfuse:", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
38
lib/utils.ts
38
lib/utils.ts
@@ -328,6 +328,44 @@ export function replaceXMLParts(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fifth try: Match by mxCell id attribute
|
||||||
|
// Extract id from search pattern and find the element with that id
|
||||||
|
if (!matchFound) {
|
||||||
|
const idMatch = search.match(/id="([^"]+)"/);
|
||||||
|
if (idMatch) {
|
||||||
|
const searchId = idMatch[1];
|
||||||
|
// Find lines that contain this id
|
||||||
|
for (let i = startLineNum; i < resultLines.length; i++) {
|
||||||
|
if (resultLines[i].includes(`id="${searchId}"`)) {
|
||||||
|
// Found the element with matching id
|
||||||
|
// Now find the extent of this element (it might span multiple lines)
|
||||||
|
let endLine = i + 1;
|
||||||
|
const line = resultLines[i].trim();
|
||||||
|
|
||||||
|
// Check if it's a self-closing tag or has children
|
||||||
|
if (!line.endsWith('/>')) {
|
||||||
|
// Find the closing tag or the end of the mxCell block
|
||||||
|
let depth = 1;
|
||||||
|
while (endLine < resultLines.length && depth > 0) {
|
||||||
|
const currentLine = resultLines[endLine].trim();
|
||||||
|
if (currentLine.startsWith('<') && !currentLine.startsWith('</') && !currentLine.endsWith('/>')) {
|
||||||
|
depth++;
|
||||||
|
} else if (currentLine.startsWith('</')) {
|
||||||
|
depth--;
|
||||||
|
}
|
||||||
|
endLine++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
matchStartLine = i;
|
||||||
|
matchEndLine = endLine;
|
||||||
|
matchFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!matchFound) {
|
if (!matchFound) {
|
||||||
throw new Error(`Search pattern not found in the diagram. The pattern may not exist in the current structure.`);
|
throw new Error(`Search pattern not found in the diagram. The pattern may not exist in the current structure.`);
|
||||||
}
|
}
|
||||||
|
|||||||
583
package-lock.json
generated
583
package-lock.json
generated
@@ -24,6 +24,7 @@
|
|||||||
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
||||||
"@radix-ui/react-dialog": "^1.1.6",
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||||
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
"@radix-ui/react-tooltip": "^1.1.8",
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
"@vercel/analytics": "^1.5.0",
|
"@vercel/analytics": "^1.5.0",
|
||||||
@@ -2328,6 +2329,103 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-collection": {
|
||||||
|
"version": "1.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
||||||
|
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-slot": "1.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-compose-refs": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-context": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-compose-refs": {
|
"node_modules/@radix-ui/react-compose-refs": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
|
||||||
@@ -2628,6 +2726,443 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-select": {
|
||||||
|
"version": "2.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
|
||||||
|
"integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/number": "1.1.1",
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-collection": "1.1.7",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-direction": "1.1.1",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||||
|
"@radix-ui/react-focus-guards": "1.1.3",
|
||||||
|
"@radix-ui/react-focus-scope": "1.1.7",
|
||||||
|
"@radix-ui/react-id": "1.1.1",
|
||||||
|
"@radix-ui/react-popper": "1.2.8",
|
||||||
|
"@radix-ui/react-portal": "1.1.9",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-slot": "1.2.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||||
|
"@radix-ui/react-use-previous": "1.1.1",
|
||||||
|
"@radix-ui/react-visually-hidden": "1.2.3",
|
||||||
|
"aria-hidden": "^1.2.4",
|
||||||
|
"react-remove-scroll": "^2.6.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/number": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/primitive": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-arrow": {
|
||||||
|
"version": "1.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
|
||||||
|
"integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.1.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-compose-refs": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-context": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-direction": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": {
|
||||||
|
"version": "1.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
|
||||||
|
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-escape-keydown": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-guards": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-scope": {
|
||||||
|
"version": "1.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
|
||||||
|
"integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-id": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/react-dom": "^2.0.0",
|
||||||
|
"@radix-ui/react-arrow": "1.1.7",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||||
|
"@radix-ui/react-use-rect": "1.1.1",
|
||||||
|
"@radix-ui/react-use-size": "1.1.1",
|
||||||
|
"@radix-ui/rect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-portal": {
|
||||||
|
"version": "1.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
|
||||||
|
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-controllable-state": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-escape-keydown": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-layout-effect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-rect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/rect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-size": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-visually-hidden": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.1.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/rect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-slot": {
|
"node_modules/@radix-ui/react-slot": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
||||||
@@ -2713,6 +3248,39 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-effect-event": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
|
||||||
|
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-use-escape-keydown": {
|
"node_modules/@radix-ui/react-use-escape-keydown": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
|
||||||
@@ -2746,6 +3314,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-previous": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-use-rect": {
|
"node_modules/@radix-ui/react-use-rect": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
||||||
"@radix-ui/react-dialog": "^1.1.6",
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||||
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
"@radix-ui/react-tooltip": "^1.1.8",
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
"@vercel/analytics": "^1.5.0",
|
"@vercel/analytics": "^1.5.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user