This commit is contained in:
Semesse 2023-03-17 13:49:35 +00:00
parent 1bf663e636
commit 292dc579d7
8 changed files with 2332 additions and 406 deletions

View File

@ -1,6 +1,8 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
// basePath: process.env.NODE_ENV === "production" ? "/web" : "/debug",
basePath: "/web",
reactStrictMode: true, reactStrictMode: true,
} };
module.exports = nextConfig module.exports = nextConfig;

View File

@ -10,14 +10,30 @@
}, },
"dependencies": { "dependencies": {
"@next/font": "13.1.6", "@next/font": "13.1.6",
"@tanstack/react-query": "^4.24.6",
"@types/node": "18.13.0", "@types/node": "18.13.0",
"@types/react": "18.0.28", "@types/react": "18.0.28",
"@types/react-dom": "18.0.10", "@types/react-dom": "18.0.10",
"clsx": "^1.2.1",
"eslint": "8.34.0", "eslint": "8.34.0",
"eslint-config-next": "13.1.6", "eslint-config-next": "13.1.6",
"next": "13.1.6", "next": "13.1.6",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "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" "typescript": "4.9.5"
},
"devDependencies": {
"@types/styled-components": "^5.1.26",
"autoprefixer": "^10.4.13",
"postcss": "^8.4.21",
"tailwindcss": "^3.2.6"
} }
} }

View File

@ -1,123 +1,524 @@
import Head from 'next/head' import Head from "next/head";
import Image from 'next/image' import Image from "next/image";
import { Inter } from '@next/font/google' import { Inter } from "@next/font/google";
import styles from '@/styles/Home.module.css' 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 ( return (
<> <>
<Head> <Head>
<title>Create Next App</title> <title>Vaala Chat</title>
<meta name="description" content="Generated by create next app" /> <meta name="description" content="Chat to small vaala" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" /> <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> </Head>
<main className={styles.main}> <main
<div className={styles.description}> className={cn(styles.main, "px-2 md:px-16 xl:px-64")}
<p> style={{ width: "100dvw", height: "100dvh" }}
Get started by editing&nbsp;
<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{' '} <div style={{ maxHeight: "calc(100dvh - 75px)", overflowY: "auto" }}>
<Image {context?.map((qa, i) => {
src="/vercel.svg" return (
alt="Vercel Logo" <div
className={styles.vercelLogo} key={i}
width={100} className="flex w-full flex-col items-stretch justify-start"
height={24} >
priority <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}
/> />
</a> ) : (
</div> <>
</div> <div>{`🐳: 思考中`}</div>
<svg
<div className={styles.center}> // xmlns="http://www.w3.org/2000/svg"
<Image // xmlns:xlink="http://www.w3.org/1999/xlink"
className={styles.logo} // style="margin: auto; background: rgb(241, 242, 243); display: block;"
src="/next.svg" width="18px"
alt="Next.js Logo" height="18px"
width={180} viewBox="0 0 100 100"
height={37} preserveAspectRatio="xMidYMid"
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"
> >
<h2 className={inter.className}> <circle cx="27.5" cy="57.5" r="5" fill="#85a2b6">
Docs <span>-&gt;</span> <animate
</h2> attributeName="cy"
<p className={inter.className}> calcMode="spline"
Find in-depth information about Next.js features and&nbsp;API. keySplines="0 0.5 0.5 1;0.5 0 1 0.5;0.5 0.5 0.5 0.5"
</p> repeatCount="indefinite"
</a> values="57.5;42.5;57.5;57.5"
keyTimes="0;0.3;0.6;1"
<a dur="1s"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app" begin="-0.6s"
className={styles.card} ></animate>
target="_blank" </circle>{" "}
rel="noopener noreferrer" <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="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" }}
> >
<h2 className={inter.className}> <textarea
Learn <span>-&gt;</span> rows={1}
</h2> className="flex-1 border-sky-500 border border-solid focus:border-sky-300 focus:outline-none rounded p-2"
<p className={inter.className}> placeholder="..."
Learn about Next.js in an interactive course with&nbsp;quizzes! value={query}
</p> onChange={(e) => setQuery(e.target.value)}
</a> ></textarea>
<button
<a type="submit"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app" 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"
className={styles.card} onClick={() => handleAsk.mutate()}
target="_blank" onSubmit={() => handleAsk.mutate()}
rel="noopener noreferrer"
> >
<h2 className={inter.className}> Send
Templates <span>-&gt;</span> </button>
</h2> <button
<p className={inter.className}> 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"
Discover and deploy boilerplate example Next.js&nbsp;projects. onClick={() => setContext(contexts as any)}
</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}> Clear
Deploy <span>-&gt;</span> </button>
</h2>
<p className={inter.className}>
Instantly deploy your Next.js site to a shareable URL
with&nbsp;Vercel.
</p>
</a>
</div> </div>
</main> </main>
</> </>
) );
}
export default function Wrapper() {
return (
<QueryClientProvider client={queryClient}>
<Home />
</QueryClientProvider>
);
} }

1801
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -1,278 +1,6 @@
.main { .main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between;
align-items: center; align-items: center;
padding: 6rem; min-height: 100dvh;
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);
}
} }

View File

@ -1,3 +1,6 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root { :root {
--max-width: 1100px; --max-width: 1100px;
--border-radius: 12px; --border-radius: 12px;

15
tailwind.config.js Normal file
View 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: [],
}