mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-06 12:27:44 +00:00
docs: add ai chat assistant for better-auth docs (#4735)
This commit is contained in:
committed by
GitHub
parent
6e891d853b
commit
f62675e8bd
85
docs/app/api/ai-chat/route.ts
Normal file
85
docs/app/api/ai-chat/route.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const gurubasePayload = {
|
||||||
|
question: body.question,
|
||||||
|
stream: body.stream,
|
||||||
|
external_user_id: body.external_user_id,
|
||||||
|
session_id: body.session_id,
|
||||||
|
fetch_existing: body.fetch_existing || false,
|
||||||
|
};
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.gurubase.io/api/v1/${process.env.GURUBASE_SLUG}/answer/`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"x-api-key": `${process.env.GURUBASE_API_KEY}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(gurubasePayload),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
if (response.status === 400) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error:
|
||||||
|
"I'm sorry, I couldn't process that question. Please try asking something else about Better-Auth.",
|
||||||
|
},
|
||||||
|
{ status: 200 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: `External API error: ${response.status} ${errorText}` },
|
||||||
|
{ status: response.status },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const isStreaming = gurubasePayload.stream === true;
|
||||||
|
if (isStreaming) {
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
if (!reader) {
|
||||||
|
controller.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pump(): Promise<void> {
|
||||||
|
return reader!.read().then(({ done, value }) => {
|
||||||
|
if (done) {
|
||||||
|
controller.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
controller.enqueue(value);
|
||||||
|
return pump();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return pump();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new NextResponse(stream, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/plain; charset=utf-8",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
Connection: "keep-alive",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const data = await response.json();
|
||||||
|
return NextResponse.json(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: `Proxy error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
|
},
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -300,3 +300,76 @@ html {
|
|||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdown-content {
|
||||||
|
@apply text-sm leading-relaxed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content pre {
|
||||||
|
@apply max-w-full overflow-x-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content pre code {
|
||||||
|
@apply whitespace-pre-wrap break-words;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content h1,
|
||||||
|
.markdown-content h2,
|
||||||
|
.markdown-content h3,
|
||||||
|
.markdown-content h4,
|
||||||
|
.markdown-content h5,
|
||||||
|
.markdown-content h6 {
|
||||||
|
@apply font-semibold text-foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content p {
|
||||||
|
@apply mb-2 last:mb-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content ul,
|
||||||
|
.markdown-content ol {
|
||||||
|
@apply space-y-2 list-disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content li {
|
||||||
|
@apply text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content code {
|
||||||
|
@apply bg-muted px-1.5 py-0.5 rounded text-xs font-mono;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content pre {
|
||||||
|
@apply overflow-x-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content blockquote {
|
||||||
|
@apply border-l-4 border-muted-foreground/20 pl-4 my-2 italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content table {
|
||||||
|
@apply w-full border-collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content th,
|
||||||
|
.markdown-content td {
|
||||||
|
@apply border border-border px-2 py-1 text-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content th {
|
||||||
|
@apply bg-muted font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes stream-pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.streaming-cursor {
|
||||||
|
animation: stream-pulse 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|||||||
426
docs/components/ai-chat-modal.tsx
Normal file
426
docs/components/ai-chat-modal.tsx
Normal file
@@ -0,0 +1,426 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Send, Loader2, Bot, User, AlertCircle } from "lucide-react";
|
||||||
|
import { MarkdownRenderer } from "./markdown-renderer";
|
||||||
|
import { betterFetch } from "@better-fetch/fetch";
|
||||||
|
import { atom } from "jotai";
|
||||||
|
|
||||||
|
interface Message {
|
||||||
|
id: string;
|
||||||
|
role: "user" | "assistant";
|
||||||
|
content: string;
|
||||||
|
timestamp: Date;
|
||||||
|
isStreaming?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const aiChatModalAtom = atom(false);
|
||||||
|
|
||||||
|
interface AIChatModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AIChatModal({ isOpen, onClose }: AIChatModalProps) {
|
||||||
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
|
const [input, setInput] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [apiError, setApiError] = useState<string | null>(null);
|
||||||
|
const [sessionId, setSessionId] = useState<string | null>(null);
|
||||||
|
const [externalUserId] = useState<string>(
|
||||||
|
() =>
|
||||||
|
`better-auth-user-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||||
|
);
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (abortControllerRef.current) {
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) {
|
||||||
|
setSessionId(null);
|
||||||
|
setMessages([]);
|
||||||
|
setInput("");
|
||||||
|
setApiError(null);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!input.trim() || isLoading) return;
|
||||||
|
|
||||||
|
const userMessage: Message = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
role: "user",
|
||||||
|
content: input.trim(),
|
||||||
|
timestamp: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages((prev) => [...prev, userMessage]);
|
||||||
|
setInput("");
|
||||||
|
setIsLoading(true);
|
||||||
|
setApiError(null);
|
||||||
|
|
||||||
|
const thinkingMessage: Message = {
|
||||||
|
id: `thinking-${Date.now()}`,
|
||||||
|
role: "assistant",
|
||||||
|
content: "",
|
||||||
|
timestamp: new Date(),
|
||||||
|
isStreaming: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages((prev) => [...prev, thinkingMessage]);
|
||||||
|
|
||||||
|
abortControllerRef.current = new AbortController();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
question: userMessage.content,
|
||||||
|
stream: false, // Use non-streaming to get session_id
|
||||||
|
session_id: sessionId, // Use existing session_id if available
|
||||||
|
external_user_id: externalUserId, // Use consistent external_user_id for consistency on getting the context right
|
||||||
|
fetch_existing: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, error } = await betterFetch<{
|
||||||
|
content?: string;
|
||||||
|
answer?: string;
|
||||||
|
response?: string;
|
||||||
|
session_id?: string;
|
||||||
|
}>("/api/ai-chat", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
signal: abortControllerRef.current.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("API Error Response:", error);
|
||||||
|
throw new Error(`HTTP ${error.status}: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.session_id) {
|
||||||
|
setSessionId(data.session_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let answer = "";
|
||||||
|
if (data.content) {
|
||||||
|
answer = data.content;
|
||||||
|
} else if (data.answer) {
|
||||||
|
answer = data.answer;
|
||||||
|
} else if (data.response) {
|
||||||
|
answer = data.response;
|
||||||
|
} else if (typeof data === "string") {
|
||||||
|
answer = data;
|
||||||
|
} else {
|
||||||
|
console.error("Unexpected response format:", data);
|
||||||
|
throw new Error("Unexpected response format from API");
|
||||||
|
}
|
||||||
|
|
||||||
|
await simulateStreamingEffect(answer, thinkingMessage.id);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.name === "AbortError") {
|
||||||
|
console.log("Request was aborted");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("Error calling AI API:", error);
|
||||||
|
|
||||||
|
setMessages((prev) =>
|
||||||
|
prev.map((msg) =>
|
||||||
|
msg.id.startsWith("thinking-")
|
||||||
|
? {
|
||||||
|
id: (Date.now() + 1).toString(),
|
||||||
|
role: "assistant" as const,
|
||||||
|
content: `I encountered an error while processing your request. Please try again.`,
|
||||||
|
timestamp: new Date(),
|
||||||
|
isStreaming: false,
|
||||||
|
}
|
||||||
|
: msg,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
setApiError(error.message);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
abortControllerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const simulateStreamingEffect = async (
|
||||||
|
fullContent: string,
|
||||||
|
thinkingMessageId: string,
|
||||||
|
) => {
|
||||||
|
const assistantMessageId = (Date.now() + 1).toString();
|
||||||
|
let displayedContent = "";
|
||||||
|
|
||||||
|
setMessages((prev) =>
|
||||||
|
prev.map((msg) =>
|
||||||
|
msg.id === thinkingMessageId
|
||||||
|
? {
|
||||||
|
id: assistantMessageId,
|
||||||
|
role: "assistant" as const,
|
||||||
|
content: "",
|
||||||
|
timestamp: new Date(),
|
||||||
|
isStreaming: true,
|
||||||
|
}
|
||||||
|
: msg,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const words = fullContent.split(" ");
|
||||||
|
for (let i = 0; i < words.length; i++) {
|
||||||
|
displayedContent += (i > 0 ? " " : "") + words[i];
|
||||||
|
|
||||||
|
setMessages((prev) =>
|
||||||
|
prev.map((msg) =>
|
||||||
|
msg.id === assistantMessageId
|
||||||
|
? { ...msg, content: displayedContent }
|
||||||
|
: msg,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const delay = Math.random() * 50 + 20;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessages((prev) =>
|
||||||
|
prev.map((msg) =>
|
||||||
|
msg.id === assistantMessageId ? { ...msg, isStreaming: false } : msg,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="max-w-4xl border-b h-[80vh] flex flex-col">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Bot className="h-5 w-5 text-primary" />
|
||||||
|
Ask AI About Better Auth
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Ask questions about Better-Auth and get AI-powered answers
|
||||||
|
{apiError && (
|
||||||
|
<div className="flex items-center gap-2 mt-2 text-amber-600 dark:text-amber-400">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<span className="text-xs">
|
||||||
|
API Error: Something went wrong. Please try again.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex-1 flex flex-col min-h-0">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex-1 overflow-y-auto space-y-4 p-6",
|
||||||
|
messages.length === 0 ? "overflow-y-hidden" : "overflow-y-auto",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{messages.length === 0 ? (
|
||||||
|
<div className="flex h-full flex-col items-center justify-center text-center">
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="w-16 h-16 mx-auto bg-transparent border border-input/70 border-dashed rounded-none flex items-center justify-center mb-4">
|
||||||
|
<Bot className="h-8 w-8 text-primary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-8 max-w-md">
|
||||||
|
<h3 className="text-xl font-semibold text-foreground mb-2">
|
||||||
|
Ask About Better Auth
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||||
|
I'm here to help you with Better Auth questions, setup
|
||||||
|
guides, and implementation tips. Ask me anything!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full max-w-lg">
|
||||||
|
<p className="text-sm font-medium text-foreground mb-4">
|
||||||
|
Try asking:
|
||||||
|
</p>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{[
|
||||||
|
"How do I set up SSO with Google?",
|
||||||
|
"How to integrate Better Auth with NextJs?",
|
||||||
|
"How to setup Two Factor Authentication?",
|
||||||
|
].map((question, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => setInput(question)}
|
||||||
|
className="w-full text-left p-3 rounded-none border border-border/50 hover:border-primary/50 hover:bg-primary/5 transition-all duration-200 group"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-6 h-6 rounded-none bg-transparent border border-input/70 border-dashed flex items-center justify-center group-hover:bg-primary/20 transition-colors">
|
||||||
|
<span className="text-xs text-primary font-medium">
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-foreground group-hover:text-primary transition-colors">
|
||||||
|
{question}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
messages.map((message) => (
|
||||||
|
<div
|
||||||
|
key={message.id}
|
||||||
|
className={cn(
|
||||||
|
"flex gap-3",
|
||||||
|
message.role === "user" ? "justify-end" : "justify-start",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{message.role === "assistant" && (
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-transparent border border-input/70 border-dashed flex items-center justify-center">
|
||||||
|
<Bot className="h-4 w-4 text-primary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"max-w-[80%] rounded-xl px-4 py-3 shadow-sm",
|
||||||
|
message.role === "user"
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "bg-background border border-border/50",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{message.role === "assistant" ? (
|
||||||
|
<div className="w-full">
|
||||||
|
{message.id.startsWith("thinking-") ? (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<div className="flex space-x-1">
|
||||||
|
<div className="w-1 h-1 bg-primary rounded-full animate-bounce [animation-delay:-0.3s]"></div>
|
||||||
|
<div className="w-1 h-1 bg-primary rounded-full animate-bounce [animation-delay:-0.15s]"></div>
|
||||||
|
<div className="w-1 h-1 bg-primary rounded-full animate-bounce"></div>
|
||||||
|
</div>
|
||||||
|
<span>Thinking...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<MarkdownRenderer content={message.content} />
|
||||||
|
{message.isStreaming && (
|
||||||
|
<div className="inline-block w-2 h-4 bg-primary streaming-cursor ml-1" />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm">{message.content}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{message.role === "user" && (
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center">
|
||||||
|
<User className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t px-0 bg-background/50 backdrop-blur-sm p-4">
|
||||||
|
<div className="relative max-w-4xl mx-auto">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative flex flex-col border-input rounded-lg transition-all duration-200 w-full text-left",
|
||||||
|
"ring-1 ring-border/20 bg-muted/30 border-input border-1 backdrop-blur-sm",
|
||||||
|
"focus-within:ring-primary/30 focus-within:bg-muted/[35%]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="overflow-y-auto max-h-[200px]">
|
||||||
|
<Textarea
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
placeholder="Ask a question about Better-Auth..."
|
||||||
|
className="w-full rounded-none rounded-b-none px-4 py-3 h-[70px] bg-transparent border-none text-foreground placeholder:text-muted-foreground resize-none focus-visible:ring-0 leading-[1.2] min-h-[52px] max-h-32"
|
||||||
|
disabled={isLoading}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
void handleSubmit(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-12 bg-muted/20 rounded-b-xl flex items-center justify-end px-3">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
void handleSubmit(e);
|
||||||
|
}}
|
||||||
|
disabled={!input.trim() || isLoading}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg p-2 transition-all duration-200",
|
||||||
|
input.trim() && !isLoading
|
||||||
|
? "bg-primary text-primary-foreground hover:bg-primary/90 hover:shadow-md"
|
||||||
|
: "bg-muted/50 text-muted-foreground cursor-not-allowed",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="w-4 h-4 border-2 border-current/30 border-t-current rounded-full animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Send className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 text-center">
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Press{" "}
|
||||||
|
<kbd className="px-1.5 py-0.5 text-xs bg-muted rounded">
|
||||||
|
Enter
|
||||||
|
</kbd>{" "}
|
||||||
|
to send,{" "}
|
||||||
|
<kbd className="px-1.5 py-0.5 text-xs bg-muted rounded">
|
||||||
|
Shift+Enter
|
||||||
|
</kbd>{" "}
|
||||||
|
for new line
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
52
docs/components/ask-ai-button.tsx
Normal file
52
docs/components/ask-ai-button.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { AIChatModal } from "@/components/ai-chat-modal";
|
||||||
|
|
||||||
|
interface AskAIButtonProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AskAIButton({ className }: AskAIButtonProps) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="text-primary"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 2L13.09 8.26L20 9L13.09 9.74L12 16L10.91 9.74L4 9L10.91 8.26L12 2Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M19 15L19.5 17.5L22 18L19.5 18.5L19 21L18.5 18.5L16 18L18.5 17.5L19 15Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M5 15L5.5 17.5L8 18L5.5 18.5L5 21L4.5 18.5L2 18L4.5 17.5L5 15Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="hidden md:inline">Ask AI</span>
|
||||||
|
</Button>
|
||||||
|
<AIChatModal isOpen={isOpen} onClose={() => setIsOpen(false)} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
120
docs/components/markdown-renderer.tsx
Normal file
120
docs/components/markdown-renderer.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
import rehypeHighlight from "rehype-highlight";
|
||||||
|
import "highlight.js/styles/dark.css";
|
||||||
|
import { Pre } from "fumadocs-ui/components/codeblock";
|
||||||
|
|
||||||
|
interface MarkdownRendererProps {
|
||||||
|
content: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MarkdownRenderer({
|
||||||
|
content,
|
||||||
|
className = "",
|
||||||
|
}: MarkdownRendererProps) {
|
||||||
|
return (
|
||||||
|
<div className={`markdown-content px-2 ${className}`}>
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
rehypePlugins={[[rehypeHighlight]]}
|
||||||
|
components={{
|
||||||
|
pre: (props) => (
|
||||||
|
<div className="my-4 max-w-full overflow-hidden">
|
||||||
|
<Pre {...props} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
|
||||||
|
code: ({ className, children, ...props }: any) => {
|
||||||
|
const isInline = !className?.includes("language-");
|
||||||
|
|
||||||
|
if (isInline) {
|
||||||
|
return (
|
||||||
|
<code
|
||||||
|
className="bg-muted px-1.5 py-0.5 rounded text-xs font-mono"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<code className={className} {...props}>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
h1: ({ children }) => (
|
||||||
|
<h1 className="text-lg font-bold mt-6 mb-3 first:mt-0">
|
||||||
|
{children}
|
||||||
|
</h1>
|
||||||
|
),
|
||||||
|
h2: ({ children }) => (
|
||||||
|
<h2 className="text-base font-semibold mt-5 mb-3">{children}</h2>
|
||||||
|
),
|
||||||
|
h3: ({ children }) => (
|
||||||
|
<h3 className="text-sm font-semibold mt-4 mb-2">{children}</h3>
|
||||||
|
),
|
||||||
|
h4: ({ children }) => (
|
||||||
|
<h4 className="text-sm font-medium mt-3 mb-2">{children}</h4>
|
||||||
|
),
|
||||||
|
h5: ({ children }) => (
|
||||||
|
<h5 className="text-xs font-medium mt-3 mb-2">{children}</h5>
|
||||||
|
),
|
||||||
|
h6: ({ children }) => (
|
||||||
|
<h6 className="text-xs font-medium mt-3 mb-2">{children}</h6>
|
||||||
|
),
|
||||||
|
|
||||||
|
p: ({ children }) => (
|
||||||
|
<p className="text-sm leading-relaxed mb-3 last:mb-0">{children}</p>
|
||||||
|
),
|
||||||
|
|
||||||
|
a: ({ href, children }) => (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary underline hover:text-primary/80 text-sm transition-colors"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
blockquote: ({ children }) => (
|
||||||
|
<blockquote className="border-l-4 border-muted-foreground/20 pl-4 my-4 text-sm italic">
|
||||||
|
{children}
|
||||||
|
</blockquote>
|
||||||
|
),
|
||||||
|
|
||||||
|
table: ({ children }) => (
|
||||||
|
<div className="overflow-x-auto my-4 max-w-full">
|
||||||
|
<table className="min-w-full text-sm border-collapse border border-border">
|
||||||
|
{children}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
th: ({ children }) => (
|
||||||
|
<th className="border border-border px-2 py-1 bg-muted text-left font-medium">
|
||||||
|
{children}
|
||||||
|
</th>
|
||||||
|
),
|
||||||
|
td: ({ children }) => (
|
||||||
|
<td className="border border-border px-2 py-1">{children}</td>
|
||||||
|
),
|
||||||
|
|
||||||
|
hr: () => <hr className="my-6 border-border" />,
|
||||||
|
|
||||||
|
strong: ({ children }) => (
|
||||||
|
<strong className="font-semibold">{children}</strong>
|
||||||
|
),
|
||||||
|
em: ({ children }) => <em className="italic">{children}</em>,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -15,6 +15,10 @@ import {
|
|||||||
import { useDocsSearch } from "fumadocs-core/search/client";
|
import { useDocsSearch } from "fumadocs-core/search/client";
|
||||||
import { OramaClient } from "@oramacloud/client";
|
import { OramaClient } from "@oramacloud/client";
|
||||||
import { useI18n } from "fumadocs-ui/contexts/i18n";
|
import { useI18n } from "fumadocs-ui/contexts/i18n";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Bot } from "lucide-react";
|
||||||
|
import { AIChatModal, aiChatModalAtom } from "./ai-chat-modal";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
|
||||||
const client = new OramaClient({
|
const client = new OramaClient({
|
||||||
endpoint: process.env.NEXT_PUBLIC_ORAMA_ENDPOINT!,
|
endpoint: process.env.NEXT_PUBLIC_ORAMA_ENDPOINT!,
|
||||||
@@ -23,37 +27,63 @@ const client = new OramaClient({
|
|||||||
|
|
||||||
export function CustomSearchDialog(props: SharedProps) {
|
export function CustomSearchDialog(props: SharedProps) {
|
||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
|
const [isAIModalOpen, setIsAIModalOpen] = useAtom(aiChatModalAtom);
|
||||||
|
|
||||||
const { search, setSearch, query } = useDocsSearch({
|
const { search, setSearch, query } = useDocsSearch({
|
||||||
type: "orama-cloud",
|
type: "orama-cloud",
|
||||||
client,
|
client,
|
||||||
locale,
|
locale,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleAskAIClick = () => {
|
||||||
|
props.onOpenChange?.(false);
|
||||||
|
setIsAIModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAIModalClose = () => {
|
||||||
|
setIsAIModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SearchDialog
|
<>
|
||||||
search={search}
|
<SearchDialog
|
||||||
onSearchChange={setSearch}
|
search={search}
|
||||||
isLoading={query.isLoading}
|
onSearchChange={setSearch}
|
||||||
{...props}
|
isLoading={query.isLoading}
|
||||||
>
|
{...props}
|
||||||
<SearchDialogOverlay />
|
>
|
||||||
<SearchDialogContent className="mt-12 md:mt-0">
|
<SearchDialogOverlay />
|
||||||
<SearchDialogHeader>
|
<SearchDialogContent className="mt-12 md:mt-0">
|
||||||
<SearchDialogIcon />
|
<SearchDialogHeader>
|
||||||
<SearchDialogInput />
|
<SearchDialogIcon />
|
||||||
<SearchDialogClose className="hidden md:block" />
|
<SearchDialogInput />
|
||||||
</SearchDialogHeader>
|
<Button
|
||||||
<SearchDialogList items={query.data !== "empty" ? query.data : null} />
|
variant="ghost"
|
||||||
<SearchDialogFooter>
|
size="sm"
|
||||||
<a
|
onClick={handleAskAIClick}
|
||||||
href="https://orama.com"
|
className="flex items-center gap-2 mr-2 text-sm hover:bg-muted"
|
||||||
rel="noreferrer noopener"
|
>
|
||||||
className="ms-auto text-xs text-fd-muted-foreground"
|
<Bot className="h-4 w-4" />
|
||||||
>
|
Ask AI
|
||||||
Search powered by Orama
|
</Button>
|
||||||
</a>
|
<SearchDialogClose className="hidden md:block" />
|
||||||
</SearchDialogFooter>
|
</SearchDialogHeader>
|
||||||
</SearchDialogContent>
|
<SearchDialogList
|
||||||
</SearchDialog>
|
items={query.data !== "empty" ? query.data : null}
|
||||||
|
/>
|
||||||
|
<SearchDialogFooter>
|
||||||
|
<a
|
||||||
|
href="https://orama.com"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
className="ms-auto text-xs text-fd-muted-foreground"
|
||||||
|
>
|
||||||
|
Search powered by Orama
|
||||||
|
</a>
|
||||||
|
</SearchDialogFooter>
|
||||||
|
</SearchDialogContent>
|
||||||
|
</SearchDialog>
|
||||||
|
|
||||||
|
<AIChatModal isOpen={isAIModalOpen} onClose={handleAIModalClose} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
43
pnpm-lock.yaml
generated
43
pnpm-lock.yaml
generated
@@ -463,7 +463,7 @@ importers:
|
|||||||
version: 1.4.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
version: 1.4.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
jotai:
|
jotai:
|
||||||
specifier: ^2.13.1
|
specifier: ^2.13.1
|
||||||
version: 2.13.1(@babel/core@7.28.4)(@babel/template@7.27.2)(@types/react@19.1.12)(react@19.1.1)
|
version: 2.14.0(@babel/core@7.28.4)(@babel/template@7.27.2)(@types/react@19.1.12)(react@19.1.1)
|
||||||
js-beautify:
|
js-beautify:
|
||||||
specifier: ^1.15.4
|
specifier: ^1.15.4
|
||||||
version: 1.15.4
|
version: 1.15.4
|
||||||
@@ -8303,8 +8303,8 @@ packages:
|
|||||||
jose@6.1.0:
|
jose@6.1.0:
|
||||||
resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==}
|
resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==}
|
||||||
|
|
||||||
jotai@2.13.1:
|
jotai@2.14.0:
|
||||||
resolution: {integrity: sha512-cRsw6kFeGC9Z/D3egVKrTXRweycZ4z/k7i2MrfCzPYsL9SIWcPXTyqv258/+Ay8VUEcihNiE/coBLE6Kic6b8A==}
|
resolution: {integrity: sha512-JQkNkTnqjk1BlSUjHfXi+pGG/573bVN104gp6CymhrWDseZGDReTNniWrLhJ+zXbM6pH+82+UNJ2vwYQUkQMWQ==}
|
||||||
engines: {node: '>=12.20.0'}
|
engines: {node: '>=12.20.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/core': '>=7.0.0'
|
'@babel/core': '>=7.0.0'
|
||||||
@@ -8746,9 +8746,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==}
|
resolution: {integrity: sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
magic-string@0.30.18:
|
|
||||||
resolution: {integrity: sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==}
|
|
||||||
|
|
||||||
magic-string@0.30.19:
|
magic-string@0.30.19:
|
||||||
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
|
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
|
||||||
|
|
||||||
@@ -15930,7 +15927,9 @@ snapshots:
|
|||||||
metro-runtime: 0.83.1
|
metro-runtime: 0.83.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
|
- bufferutil
|
||||||
- supports-color
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
'@react-native/normalize-colors@0.79.6': {}
|
'@react-native/normalize-colors@0.79.6': {}
|
||||||
|
|
||||||
@@ -17422,7 +17421,7 @@ snapshots:
|
|||||||
|
|
||||||
'@vue/compiler-core@3.5.19':
|
'@vue/compiler-core@3.5.19':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.28.3
|
'@babel/parser': 7.28.4
|
||||||
'@vue/shared': 3.5.19
|
'@vue/shared': 3.5.19
|
||||||
entities: 4.5.0
|
entities: 4.5.0
|
||||||
estree-walker: 2.0.2
|
estree-walker: 2.0.2
|
||||||
@@ -17435,7 +17434,7 @@ snapshots:
|
|||||||
|
|
||||||
'@vue/compiler-sfc@3.5.19':
|
'@vue/compiler-sfc@3.5.19':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.28.3
|
'@babel/parser': 7.28.4
|
||||||
'@vue/compiler-core': 3.5.19
|
'@vue/compiler-core': 3.5.19
|
||||||
'@vue/compiler-dom': 3.5.19
|
'@vue/compiler-dom': 3.5.19
|
||||||
'@vue/compiler-ssr': 3.5.19
|
'@vue/compiler-ssr': 3.5.19
|
||||||
@@ -17707,7 +17706,7 @@ snapshots:
|
|||||||
|
|
||||||
autoprefixer@10.4.21(postcss@8.5.6):
|
autoprefixer@10.4.21(postcss@8.5.6):
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.25.4
|
browserslist: 4.26.0
|
||||||
caniuse-lite: 1.0.30001741
|
caniuse-lite: 1.0.30001741
|
||||||
fraction.js: 4.3.7
|
fraction.js: 4.3.7
|
||||||
normalize-range: 0.1.2
|
normalize-range: 0.1.2
|
||||||
@@ -18135,7 +18134,7 @@ snapshots:
|
|||||||
|
|
||||||
caniuse-api@3.0.0:
|
caniuse-api@3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.25.4
|
browserslist: 4.26.0
|
||||||
caniuse-lite: 1.0.30001741
|
caniuse-lite: 1.0.30001741
|
||||||
lodash.memoize: 4.1.2
|
lodash.memoize: 4.1.2
|
||||||
lodash.uniq: 4.5.0
|
lodash.uniq: 4.5.0
|
||||||
@@ -18576,7 +18575,7 @@ snapshots:
|
|||||||
|
|
||||||
cssnano-preset-default@7.0.7(postcss@8.5.6):
|
cssnano-preset-default@7.0.7(postcss@8.5.6):
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.25.4
|
browserslist: 4.26.0
|
||||||
css-declaration-sorter: 7.2.0(postcss@8.5.6)
|
css-declaration-sorter: 7.2.0(postcss@8.5.6)
|
||||||
cssnano-utils: 5.0.1(postcss@8.5.6)
|
cssnano-utils: 5.0.1(postcss@8.5.6)
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
@@ -20475,7 +20474,7 @@ snapshots:
|
|||||||
|
|
||||||
jose@6.1.0: {}
|
jose@6.1.0: {}
|
||||||
|
|
||||||
jotai@2.13.1(@babel/core@7.28.4)(@babel/template@7.27.2)(@types/react@19.1.12)(react@19.1.1):
|
jotai@2.14.0(@babel/core@7.28.4)(@babel/template@7.27.2)(@types/react@19.1.12)(react@19.1.1):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@babel/core': 7.28.4
|
'@babel/core': 7.28.4
|
||||||
'@babel/template': 7.27.2
|
'@babel/template': 7.27.2
|
||||||
@@ -20887,10 +20886,6 @@ snapshots:
|
|||||||
|
|
||||||
luxon@3.7.1: {}
|
luxon@3.7.1: {}
|
||||||
|
|
||||||
magic-string@0.30.18:
|
|
||||||
dependencies:
|
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
|
||||||
|
|
||||||
magic-string@0.30.19:
|
magic-string@0.30.19:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
@@ -22671,7 +22666,7 @@ snapshots:
|
|||||||
|
|
||||||
postcss-colormin@7.0.3(postcss@8.5.6):
|
postcss-colormin@7.0.3(postcss@8.5.6):
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.25.4
|
browserslist: 4.26.0
|
||||||
caniuse-api: 3.0.0
|
caniuse-api: 3.0.0
|
||||||
colord: 2.9.3
|
colord: 2.9.3
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
@@ -22679,7 +22674,7 @@ snapshots:
|
|||||||
|
|
||||||
postcss-convert-values@7.0.5(postcss@8.5.6):
|
postcss-convert-values@7.0.5(postcss@8.5.6):
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.25.4
|
browserslist: 4.26.0
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
postcss-value-parser: 4.2.0
|
postcss-value-parser: 4.2.0
|
||||||
|
|
||||||
@@ -22708,7 +22703,7 @@ snapshots:
|
|||||||
|
|
||||||
postcss-merge-rules@7.0.5(postcss@8.5.6):
|
postcss-merge-rules@7.0.5(postcss@8.5.6):
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.25.4
|
browserslist: 4.26.0
|
||||||
caniuse-api: 3.0.0
|
caniuse-api: 3.0.0
|
||||||
cssnano-utils: 5.0.1(postcss@8.5.6)
|
cssnano-utils: 5.0.1(postcss@8.5.6)
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
@@ -22728,7 +22723,7 @@ snapshots:
|
|||||||
|
|
||||||
postcss-minify-params@7.0.3(postcss@8.5.6):
|
postcss-minify-params@7.0.3(postcss@8.5.6):
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.25.4
|
browserslist: 4.26.0
|
||||||
cssnano-utils: 5.0.1(postcss@8.5.6)
|
cssnano-utils: 5.0.1(postcss@8.5.6)
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
postcss-value-parser: 4.2.0
|
postcss-value-parser: 4.2.0
|
||||||
@@ -22775,7 +22770,7 @@ snapshots:
|
|||||||
|
|
||||||
postcss-normalize-unicode@7.0.3(postcss@8.5.6):
|
postcss-normalize-unicode@7.0.3(postcss@8.5.6):
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.25.4
|
browserslist: 4.26.0
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
postcss-value-parser: 4.2.0
|
postcss-value-parser: 4.2.0
|
||||||
|
|
||||||
@@ -22797,7 +22792,7 @@ snapshots:
|
|||||||
|
|
||||||
postcss-reduce-initial@7.0.3(postcss@8.5.6):
|
postcss-reduce-initial@7.0.3(postcss@8.5.6):
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.25.4
|
browserslist: 4.26.0
|
||||||
caniuse-api: 3.0.0
|
caniuse-api: 3.0.0
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
|
|
||||||
@@ -24188,7 +24183,7 @@ snapshots:
|
|||||||
|
|
||||||
stylehacks@7.0.5(postcss@8.5.6):
|
stylehacks@7.0.5(postcss@8.5.6):
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.25.4
|
browserslist: 4.26.0
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
postcss-selector-parser: 7.1.0
|
postcss-selector-parser: 7.1.0
|
||||||
|
|
||||||
@@ -25009,7 +25004,7 @@ snapshots:
|
|||||||
chai: 5.2.0
|
chai: 5.2.0
|
||||||
debug: 4.4.1
|
debug: 4.4.1
|
||||||
expect-type: 1.2.1
|
expect-type: 1.2.1
|
||||||
magic-string: 0.30.18
|
magic-string: 0.30.19
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
std-env: 3.9.0
|
std-env: 3.9.0
|
||||||
|
|||||||
Reference in New Issue
Block a user