From 292dc579d7f2cfbdffc60efea7e9e12c35a0c168 Mon Sep 17 00:00:00 2001 From: Semesse Date: Fri, 17 Mar 2023 13:49:35 +0000 Subject: [PATCH] chat --- next.config.js | 6 +- package.json | 16 + pages/index.tsx | 617 +++++++++++--- pnpm-lock.yaml | 1801 +++++++++++++++++++++++++++++++++++++++- postcss.config.js | 6 + styles/Home.module.css | 274 +----- styles/globals.css | 3 + tailwind.config.js | 15 + 8 files changed, 2332 insertions(+), 406 deletions(-) create mode 100644 postcss.config.js create mode 100644 tailwind.config.js diff --git a/next.config.js b/next.config.js index a843cbe..c34d150 100644 --- a/next.config.js +++ b/next.config.js @@ -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; diff --git a/package.json b/package.json index caefaea..ab1d7a3 100644 --- a/package.json +++ b/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" } } diff --git a/pages/index.tsx b/pages/index.tsx index 3a20955..5d16933 100644 --- a/pages/index.tsx +++ b/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
{typedText}
; +}; + +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 ? ( + + {String(children).replace(/\n$/, "")} + + ) : ( + + {children} + + ); + }, +}; +const Markdown = ({ children }: { children: string }) => { + return ( + + {children} + + ); +}; + +const ask = async ( + query: string, + context: [string, string][], + jwt?: string +) => { + const res = await fetch(`/ai/ask`, { + method: "POST", + headers: { + Authorization: jwt, + } as Record, + 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 ( <> - Create Next App - + Vaala Chat + +