chat
This commit is contained in:
parent
1bf663e636
commit
292dc579d7
@ -1,6 +1,8 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// basePath: process.env.NODE_ENV === "production" ? "/web" : "/debug",
|
||||
basePath: "/web",
|
||||
reactStrictMode: true,
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = nextConfig
|
||||
module.exports = nextConfig;
|
||||
|
16
package.json
16
package.json
@ -10,14 +10,30 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/font": "13.1.6",
|
||||
"@tanstack/react-query": "^4.24.6",
|
||||
"@types/node": "18.13.0",
|
||||
"@types/react": "18.0.28",
|
||||
"@types/react-dom": "18.0.10",
|
||||
"clsx": "^1.2.1",
|
||||
"eslint": "8.34.0",
|
||||
"eslint-config-next": "13.1.6",
|
||||
"next": "13.1.6",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-markdown": "^8.0.5",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-use": "^17.4.0",
|
||||
"rehype-highlight": "^6.0.0",
|
||||
"rehype-pretty-code": "^0.9.4",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"shiki": "^0.14.1",
|
||||
"styled-components": "^5.3.8",
|
||||
"typescript": "4.9.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/styled-components": "^5.1.26",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"postcss": "^8.4.21",
|
||||
"tailwindcss": "^3.2.6"
|
||||
}
|
||||
}
|
||||
|
617
pages/index.tsx
617
pages/index.tsx
@ -1,123 +1,524 @@
|
||||
import Head from 'next/head'
|
||||
import Image from 'next/image'
|
||||
import { Inter } from '@next/font/google'
|
||||
import styles from '@/styles/Home.module.css'
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import { Inter } from "@next/font/google";
|
||||
import styles from "@/styles/Home.module.css";
|
||||
import {
|
||||
QueryClientProvider,
|
||||
QueryClient,
|
||||
useQuery,
|
||||
useMutation,
|
||||
} from "@tanstack/react-query";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { useKey } from "react-use";
|
||||
import cn from "clsx";
|
||||
// import rehypePrettyCode from "rehype-pretty-code";
|
||||
// import * as shiki from "shiki";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import styled from "styled-components";
|
||||
// import rehypeHighlight from "rehype-highlight";
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
// @ts-ignore
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
// @ts-ignore
|
||||
import theme from "react-syntax-highlighter/dist/cjs/styles/prism/vs-dark";
|
||||
// const CDN_BASE = "https://npm.elemecdn.com/";
|
||||
// shiki.setCDN(`${CDN_BASE}/shiki@0.14.1/`);
|
||||
|
||||
export default function Home() {
|
||||
const SMD = styled(ReactMarkdown)`
|
||||
blockquote,
|
||||
hr,
|
||||
p {
|
||||
margin-block: 1rem;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: bold;
|
||||
margin-block: 1rem;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
color: var(--h1-color);
|
||||
}
|
||||
h2 {
|
||||
font-size: 2.5rem;
|
||||
color: var(--h2-color);
|
||||
}
|
||||
h3 {
|
||||
font-size: 2rem;
|
||||
color: var(--h3-color);
|
||||
}
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
color: var(--h4-color);
|
||||
}
|
||||
h5 {
|
||||
font-size: 1rem;
|
||||
color: var(--h5-color);
|
||||
}
|
||||
h6 {
|
||||
font-size: 0.9rem;
|
||||
color: var(--h6-color);
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.img-alt {
|
||||
display: block;
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
thead {
|
||||
border-bottom: 2px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
tr {
|
||||
line-height: normal;
|
||||
padding: 0 4px;
|
||||
background-color: var(--pre-code);
|
||||
}
|
||||
|
||||
tr:nth-child(0) {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
td {
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
td:not(:last-child) {
|
||||
border-right: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--text-a);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--text-a-hover);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 1rem 0;
|
||||
padding-inline: 2ch;
|
||||
padding-block: 0.5rem;
|
||||
background-color: var(--pre-code);
|
||||
border-left: 0.5ch solid var(--interactive-accent);
|
||||
}
|
||||
blockquote:has(> blockquote) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
blockquote > blockquote {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
blockquote:not(:has(> blockquote)) {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
background-color: var(--background-modifier-border);
|
||||
height: 1px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: revert;
|
||||
}
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
ul:not(.contains-task-list),
|
||||
ol:not(.contains-task-list) {
|
||||
padding-left: 2em;
|
||||
}
|
||||
ul.contains-task-list,
|
||||
ol.contains-task-list {
|
||||
margin-left: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const TextTyper = ({
|
||||
// now the phrase, interval and HTML element desired will come via props and we have some default values here
|
||||
text = "",
|
||||
skip = 0,
|
||||
onFinish = () => {},
|
||||
}) => {
|
||||
const [typedText, setTypedText] = useState("");
|
||||
const interval = 50;
|
||||
const step = Math.ceil(text.length / 100);
|
||||
|
||||
// @ts-ignore
|
||||
const typingRender = (text, updater, interval) => {
|
||||
let localTypingIndex = skip;
|
||||
let localTyping = text.slice(0, skip);
|
||||
if (text) {
|
||||
let printer = setInterval(() => {
|
||||
if (localTypingIndex < text.length) {
|
||||
updater(
|
||||
(localTyping += text.slice(
|
||||
localTypingIndex,
|
||||
localTypingIndex + step
|
||||
))
|
||||
);
|
||||
localTypingIndex += step;
|
||||
document.querySelector("#anchor")?.scrollIntoView();
|
||||
} else {
|
||||
localTypingIndex = 0;
|
||||
localTyping = "";
|
||||
clearInterval(printer);
|
||||
onFinish();
|
||||
}
|
||||
}, interval);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
typingRender(text, setTypedText, interval);
|
||||
}, [text, interval]);
|
||||
|
||||
return <div className="whitespace-pre-wrap">{typedText}</div>;
|
||||
};
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
const p429 = "请求过多,请稍后再试";
|
||||
|
||||
const rehypePlugins: any[] = [
|
||||
// [
|
||||
// rehypePrettyCode,
|
||||
// {
|
||||
// // theme: {
|
||||
// // dark: "github-dark",
|
||||
// // light: "github-light",
|
||||
// // },
|
||||
// theme: "github-dark",
|
||||
// keepBackground: true,
|
||||
// },
|
||||
// ],
|
||||
// rehypeHighlight,
|
||||
];
|
||||
const remarkPlugins = [remarkGfm];
|
||||
const components = {
|
||||
// @ts-ignore
|
||||
code({ node, inline, className, children, ...props }: any) {
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
return !inline && match ? (
|
||||
<SyntaxHighlighter
|
||||
style={theme}
|
||||
language={match[1] || "clike"}
|
||||
PreTag="div"
|
||||
{...props}
|
||||
>
|
||||
{String(children).replace(/\n$/, "")}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
};
|
||||
const Markdown = ({ children }: { children: string }) => {
|
||||
return (
|
||||
<SMD
|
||||
rehypePlugins={rehypePlugins}
|
||||
remarkPlugins={remarkPlugins}
|
||||
// components={components}
|
||||
>
|
||||
{children}
|
||||
</SMD>
|
||||
);
|
||||
};
|
||||
|
||||
const ask = async (
|
||||
query: string,
|
||||
context: [string, string][],
|
||||
jwt?: string
|
||||
) => {
|
||||
const res = await fetch(`/ai/ask`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: jwt,
|
||||
} as Record<string, string>,
|
||||
body: JSON.stringify({
|
||||
query: query,
|
||||
context: context
|
||||
.slice(1)
|
||||
.filter((c) => c[1] !== p429)
|
||||
.map((c) => ({ query: c[0], answer: c[1] })),
|
||||
}),
|
||||
});
|
||||
if (res.status === 429) {
|
||||
return { code: 1, msg: p429 };
|
||||
}
|
||||
return (await res.json()) as { code: number; msg: string };
|
||||
};
|
||||
|
||||
let contexts = [
|
||||
[
|
||||
"hello",
|
||||
"您好!我是小Vaala。我可以回答您的问题、写文章、写作业、翻译,对于一些法律等领域的问题我也可以给你提供信息。",
|
||||
],
|
||||
];
|
||||
|
||||
function Home() {
|
||||
const [query, setQuery] = useState("");
|
||||
const [question, setQuestion] = useState("");
|
||||
const [answer, setAnswer] = useState("");
|
||||
const [context, setContext] = useState<[string, string][]>(
|
||||
contexts as [string, string][]
|
||||
);
|
||||
const { data: jwt } = useQuery(["jwt"], async () => {
|
||||
const resp = await fetch(`/auth/jwt`);
|
||||
const res = (await resp.json())["authorization"];
|
||||
return res;
|
||||
});
|
||||
useKey("Enter", (e) => {
|
||||
if (!e.shiftKey && !e.metaKey && !e.ctrlKey && !e.isComposing) {
|
||||
e.preventDefault();
|
||||
handleAsk.mutate();
|
||||
}
|
||||
});
|
||||
const handleTypeFinish = useCallback(() => {
|
||||
setQuestion("");
|
||||
setContext((v) => [...v, [question, answer]]);
|
||||
setAnswer("");
|
||||
document.querySelector("#anchor")?.scrollIntoView();
|
||||
}, [query, answer]);
|
||||
|
||||
const handleAsk = useMutation(async () => {
|
||||
if (query.length === 0) return;
|
||||
setQuestion(query);
|
||||
setQuery("");
|
||||
|
||||
setTimeout(() => document.querySelector("#anchor")?.scrollIntoView(), 100);
|
||||
(async () => {
|
||||
const ans = await ask(query, context, jwt || "").finally(() => {
|
||||
setTimeout(
|
||||
() => document.querySelector("#anchor")?.scrollIntoView(),
|
||||
300
|
||||
);
|
||||
});
|
||||
if (ans.code) alert(ans.msg);
|
||||
else setAnswer(ans.msg);
|
||||
})();
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Create Next App</title>
|
||||
<meta name="description" content="Generated by create next app" />
|
||||
<title>Vaala Chat</title>
|
||||
<meta name="description" content="Chat to small vaala" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `.lds-dual-ring {
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
.lds-dual-ring:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin: 0 0 0 8px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #fff;
|
||||
border-color: #fff transparent #fff transparent;
|
||||
animation: lds-dual-ring 1.2s linear infinite;
|
||||
}
|
||||
@keyframes lds-dual-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
pre > code {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</Head>
|
||||
<main className={styles.main}>
|
||||
<div className={styles.description}>
|
||||
<p>
|
||||
Get started by editing
|
||||
<code className={styles.code}>pages/index.tsx</code>
|
||||
</p>
|
||||
<div>
|
||||
<a
|
||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
By{' '}
|
||||
<Image
|
||||
src="/vercel.svg"
|
||||
alt="Vercel Logo"
|
||||
className={styles.vercelLogo}
|
||||
width={100}
|
||||
height={24}
|
||||
priority
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<main
|
||||
className={cn(styles.main, "px-2 md:px-16 xl:px-64")}
|
||||
style={{ width: "100dvw", height: "100dvh" }}
|
||||
>
|
||||
<div style={{ maxHeight: "calc(100dvh - 75px)", overflowY: "auto" }}>
|
||||
{context?.map((qa, i) => {
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="flex w-full flex-col items-stretch justify-start"
|
||||
>
|
||||
<div className="p-4 w-full bg-white dark:bg-slate-800">
|
||||
<Markdown>{`ME: ${qa[0]}`}</Markdown>
|
||||
</div>
|
||||
<div className="p-4 w-full bg-slate-100 dark:bg-slate-700">
|
||||
{qa[1] === p429 ? (
|
||||
<span className="text-red-500">{qa[1]}</span>
|
||||
) : (
|
||||
<Markdown
|
||||
// remarkPlugins={[remarkGfm]}
|
||||
>
|
||||
{`🐳: ${qa[1]}`}
|
||||
</Markdown>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{question || answer ? (
|
||||
<>
|
||||
<div className="p-4 w-full bg-white dark:bg-slate-800">
|
||||
<Markdown>{`ME: ${question}`}</Markdown>
|
||||
</div>
|
||||
<div
|
||||
className="p-4 w-full flex items-center bg-slate-100 dark:bg-slate-700"
|
||||
style={{ lineHeight: "18px" }}
|
||||
>
|
||||
{answer ? (
|
||||
<TextTyper
|
||||
skip={3}
|
||||
text={`🐳: ${answer}`}
|
||||
onFinish={handleTypeFinish}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div>{`🐳: 思考中`}</div>
|
||||
<svg
|
||||
// xmlns="http://www.w3.org/2000/svg"
|
||||
// xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
// style="margin: auto; background: rgb(241, 242, 243); display: block;"
|
||||
width="18px"
|
||||
height="18px"
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
>
|
||||
<circle cx="27.5" cy="57.5" r="5" fill="#85a2b6">
|
||||
<animate
|
||||
attributeName="cy"
|
||||
calcMode="spline"
|
||||
keySplines="0 0.5 0.5 1;0.5 0 1 0.5;0.5 0.5 0.5 0.5"
|
||||
repeatCount="indefinite"
|
||||
values="57.5;42.5;57.5;57.5"
|
||||
keyTimes="0;0.3;0.6;1"
|
||||
dur="1s"
|
||||
begin="-0.6s"
|
||||
></animate>
|
||||
</circle>{" "}
|
||||
<circle cx="42.5" cy="57.5" r="5" fill="#bbcedd">
|
||||
<animate
|
||||
attributeName="cy"
|
||||
calcMode="spline"
|
||||
keySplines="0 0.5 0.5 1;0.5 0 1 0.5;0.5 0.5 0.5 0.5"
|
||||
repeatCount="indefinite"
|
||||
values="57.5;42.5;57.5;57.5"
|
||||
keyTimes="0;0.3;0.6;1"
|
||||
dur="1s"
|
||||
begin="-0.44999999999999996s"
|
||||
></animate>
|
||||
</circle>{" "}
|
||||
<circle cx="57.5" cy="57.5" r="5" fill="#dce4eb">
|
||||
<animate
|
||||
attributeName="cy"
|
||||
calcMode="spline"
|
||||
keySplines="0 0.5 0.5 1;0.5 0 1 0.5;0.5 0.5 0.5 0.5"
|
||||
repeatCount="indefinite"
|
||||
values="57.5;42.5;57.5;57.5"
|
||||
keyTimes="0;0.3;0.6;1"
|
||||
dur="1s"
|
||||
begin="-0.3s"
|
||||
></animate>
|
||||
</circle>{" "}
|
||||
<circle cx="72.5" cy="57.5" r="5" fill="#fdfdfd">
|
||||
<animate
|
||||
attributeName="cy"
|
||||
calcMode="spline"
|
||||
keySplines="0 0.5 0.5 1;0.5 0 1 0.5;0.5 0.5 0.5 0.5"
|
||||
repeatCount="indefinite"
|
||||
values="57.5;42.5;57.5;57.5"
|
||||
keyTimes="0;0.3;0.6;1"
|
||||
dur="1s"
|
||||
begin="-0.15s"
|
||||
></animate>
|
||||
</circle>
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
<div id="anchor" className="w-full h-1"></div>
|
||||
</div>
|
||||
|
||||
<div className={styles.center}>
|
||||
<Image
|
||||
className={styles.logo}
|
||||
src="/next.svg"
|
||||
alt="Next.js Logo"
|
||||
width={180}
|
||||
height={37}
|
||||
priority
|
||||
/>
|
||||
<div className={styles.thirteen}>
|
||||
<Image
|
||||
src="/thirteen.svg"
|
||||
alt="13"
|
||||
width={40}
|
||||
height={31}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.grid}>
|
||||
<a
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
<div
|
||||
className="fixed bottom-0 p-4 w-full flex justify-center items-center bg-gray-50 dark:bg-gray-900 border-t border-solid border-sky-400"
|
||||
style={{ width: "100vw" }}
|
||||
>
|
||||
<textarea
|
||||
rows={1}
|
||||
className="flex-1 border-sky-500 border border-solid focus:border-sky-300 focus:outline-none rounded p-2"
|
||||
placeholder="..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
></textarea>
|
||||
<button
|
||||
type="submit"
|
||||
className="text-white bg-sky-500 border-sky-500 border border-solid rounded h-12 p-2 ml-4 hover:border-sky-300 hover:bg-sky-300"
|
||||
onClick={() => handleAsk.mutate()}
|
||||
onSubmit={() => handleAsk.mutate()}
|
||||
>
|
||||
<h2 className={inter.className}>
|
||||
Docs <span>-></span>
|
||||
</h2>
|
||||
<p className={inter.className}>
|
||||
Find in-depth information about Next.js features and API.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
Send
|
||||
</button>
|
||||
<button
|
||||
className="text-white bg-sky-500 border-sky-500 border border-solid rounded h-12 p-2 ml-4 hover:border-sky-300 hover:bg-sky-300"
|
||||
onClick={() => setContext(contexts as any)}
|
||||
>
|
||||
<h2 className={inter.className}>
|
||||
Learn <span>-></span>
|
||||
</h2>
|
||||
<p className={inter.className}>
|
||||
Learn about Next.js in an interactive course with quizzes!
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={inter.className}>
|
||||
Templates <span>-></span>
|
||||
</h2>
|
||||
<p className={inter.className}>
|
||||
Discover and deploy boilerplate example Next.js projects.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<h2 className={inter.className}>
|
||||
Deploy <span>-></span>
|
||||
</h2>
|
||||
<p className={inter.className}>
|
||||
Instantly deploy your Next.js site to a shareable URL
|
||||
with Vercel.
|
||||
</p>
|
||||
</a>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default function Wrapper() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Home />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
1801
pnpm-lock.yaml
generated
1801
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
@ -1,278 +1,6 @@
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6rem;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.description {
|
||||
display: inherit;
|
||||
justify-content: inherit;
|
||||
align-items: inherit;
|
||||
font-size: 0.85rem;
|
||||
max-width: var(--max-width);
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.description a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.description p {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
background-color: rgba(var(--callout-rgb), 0.5);
|
||||
border: 1px solid rgba(var(--callout-border-rgb), 0.3);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.code {
|
||||
font-weight: 700;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(25%, auto));
|
||||
width: var(--max-width);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1rem 1.2rem;
|
||||
border-radius: var(--border-radius);
|
||||
background: rgba(var(--card-rgb), 0);
|
||||
border: 1px solid rgba(var(--card-border-rgb), 0);
|
||||
transition: background 200ms, border 200ms;
|
||||
}
|
||||
|
||||
.card span {
|
||||
display: inline-block;
|
||||
transition: transform 200ms;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.7rem;
|
||||
}
|
||||
|
||||
.card p {
|
||||
margin: 0;
|
||||
opacity: 0.6;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
max-width: 30ch;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
.center::before {
|
||||
background: var(--secondary-glow);
|
||||
border-radius: 50%;
|
||||
width: 480px;
|
||||
height: 360px;
|
||||
margin-left: -400px;
|
||||
}
|
||||
|
||||
.center::after {
|
||||
background: var(--primary-glow);
|
||||
width: 240px;
|
||||
height: 180px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.center::before,
|
||||
.center::after {
|
||||
content: '';
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
filter: blur(45px);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.logo,
|
||||
.thirteen {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.thirteen {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
padding: 25px 10px;
|
||||
margin-left: 16px;
|
||||
transform: translateZ(0);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
box-shadow: 0px 2px 8px -1px #0000001a;
|
||||
}
|
||||
|
||||
.thirteen::before,
|
||||
.thirteen::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Conic Gradient Animation */
|
||||
.thirteen::before {
|
||||
animation: 6s rotate linear infinite;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: var(--tile-border);
|
||||
}
|
||||
|
||||
/* Inner Square */
|
||||
.thirteen::after {
|
||||
inset: 0;
|
||||
padding: 1px;
|
||||
border-radius: var(--border-radius);
|
||||
background: linear-gradient(
|
||||
to bottom right,
|
||||
rgba(var(--tile-start-rgb), 1),
|
||||
rgba(var(--tile-end-rgb), 1)
|
||||
);
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
/* Enable hover only on non-touch devices */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.card:hover {
|
||||
background: rgba(var(--card-rgb), 0.1);
|
||||
border: 1px solid rgba(var(--card-border-rgb), 0.15);
|
||||
}
|
||||
|
||||
.card:hover span {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
.thirteen::before {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.card:hover span {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 700px) {
|
||||
.content {
|
||||
padding: 4rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
margin-bottom: 120px;
|
||||
max-width: 320px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1rem 2.5rem;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.center {
|
||||
padding: 8rem 0 6rem;
|
||||
}
|
||||
|
||||
.center::before {
|
||||
transform: none;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.description a {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.description p,
|
||||
.description div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.description p {
|
||||
align-items: center;
|
||||
inset: 0 0 auto;
|
||||
padding: 2rem 1rem 1.4rem;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(var(--background-start-rgb), 1),
|
||||
rgba(var(--callout-rgb), 0.5)
|
||||
);
|
||||
background-clip: padding-box;
|
||||
backdrop-filter: blur(24px);
|
||||
}
|
||||
|
||||
.description div {
|
||||
align-items: flex-end;
|
||||
pointer-events: none;
|
||||
inset: auto 0 0;
|
||||
padding: 2rem;
|
||||
height: 200px;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
rgb(var(--background-end-rgb)) 40%
|
||||
);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet and Smaller Desktop */
|
||||
@media (min-width: 701px) and (max-width: 1120px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, 50%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.vercelLogo {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.logo,
|
||||
.thirteen img {
|
||||
filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
min-height: 100dvh;
|
||||
}
|
||||
|
@ -1,3 +1,6 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
:root {
|
||||
--max-width: 1100px;
|
||||
--border-radius: 12px;
|
||||
|
15
tailwind.config.js
Normal file
15
tailwind.config.js
Normal file
@ -0,0 +1,15 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./app/**/*.{js,ts,jsx,tsx}",
|
||||
"./pages/**/*.{js,ts,jsx,tsx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx}",
|
||||
|
||||
// Or if using `src` directory:
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user