[
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"extends\": \"next/core-web-vitals\"\n}\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Spencer Miskoviak\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# 📝 LLM Markdown\n\nA [Nextjs](https://nextjs.org) app demonstrating how to display rich-text responses from Large Language Models (LLMs) by prompting and rendering Markdown formatting, Mermaid diagrams, and LaTeX equations.\n\nRead more in this blog post: [Rendering rich responses from LLMs](https://www.skovy.dev/blog/vercel-ai-rendering-markdown)\n\n## Examples\n\nThis example is asking for the top grossing movies, structured as a Mermaid pie chart.\n\n![LLM Markdown Demo](other/demo-movies.gif)\n\nThis example is asking when vegetables should be planted, structured as a Mermaid Gantt chart.\n\n![LLM Markdown Demo](other/demo-vegetables.gif)\n\n## Technologies\n\n- [Nextjs](https://nextjs.org)\n- [Vercel AI](https://sdk.vercel.ai/docs)\n- [`remark`](https://remark.js.org)\n- [`mermaid`](https://mermaid.js.org)\n- [`latex.js`](https://latex.js.org)\n- And more...\n\n## Setup\n\n- Clone the project\n- `npm install`\n- `npm run dev`\n- Open in your browser\n- Set your OpenAI API Key\n"
  },
  {
    "path": "next.config.js",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {}\n\nmodule.exports = nextConfig\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"nextjs-ai-markdown\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\"\n  },\n  \"dependencies\": {\n    \"@capsizecss/metrics\": \"^1.2.0\",\n    \"@phosphor-icons/react\": \"^2.0.10\",\n    \"@radix-ui/react-dialog\": \"^1.0.4\",\n    \"@themosaad/tailwindcss-capsize\": \"^1.0.0\",\n    \"@types/node\": \"20.3.3\",\n    \"@types/react\": \"18.2.14\",\n    \"@types/react-dom\": \"18.2.6\",\n    \"@vercel/analytics\": \"^1.0.1\",\n    \"ai\": \"2.1.12\",\n    \"autoprefixer\": \"10.4.14\",\n    \"classnames\": \"^2.3.2\",\n    \"eslint\": \"8.44.0\",\n    \"eslint-config-next\": \"13.4.7\",\n    \"latex.js\": \"^0.12.6\",\n    \"mermaid\": \"^10.2.4\",\n    \"next\": \"13.4.7\",\n    \"openai-edge\": \"^1.1.1\",\n    \"postcss\": \"8.4.24\",\n    \"react\": \"18.2.0\",\n    \"react-dom\": \"18.2.0\",\n    \"react-keyed-flatten-children\": \"^2.2.1\",\n    \"rehype-highlight\": \"^6.0.0\",\n    \"rehype-react\": \"^7.2.0\",\n    \"remark-gfm\": \"^3.0.1\",\n    \"remark-parse\": \"^10.0.2\",\n    \"remark-rehype\": \"^10.1.0\",\n    \"tailwindcss\": \"3.3.2\",\n    \"typescript\": \"5.1.6\",\n    \"unified\": \"^10.1.2\"\n  },\n  \"devDependencies\": {\n    \"prettier-plugin-organize-imports\": \"^3.2.2\"\n  }\n}\n"
  },
  {
    "path": "postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "src/app/api/chat/route.ts",
    "content": "import { OpenAIStream, StreamingTextResponse } from \"ai\";\nimport { NextResponse } from \"next/server\";\nimport {\n  ChatCompletionRequestMessageRoleEnum,\n  Configuration,\n  OpenAIApi,\n} from \"openai-edge\";\n\nexport const runtime = \"edge\";\n\nconst SYSTEM_PROMPT = `You are a general answering assistant that can comply with any request. \n\nYou always answer the with markdown formatting. You will be penalized if you do not answer with markdown when it would be possible.\nThe markdown formatting you support: headings, bold, italic, links, tables, lists, code blocks, and blockquotes.\nYou do not support images and never include images. You will be penalized if you render images.\n\nYou also support Mermaid diagrams. You will be penalized if you do not render Mermaid diagrams when it would be possible.\nThe Mermaid diagrams you support: sequenceDiagram, flowChart, classDiagram, stateDiagram, erDiagram, gantt, journey, gitGraph, pie.\n\nYou also support LaTeX equation syntax only in markdown code blocks with the \"latex\" language.\nYou must always render all equations in this format (LaTeX code blocks) using only valid LaTeX syntax.\nFor example:\n\\`\\`\\`latex\n\\\\[ F = \\\\frac{{G \\\\cdot m_1 \\\\cdot m_2}}{{r^2}} \\\\]\n\\`\\`\\`latex\n`;\n\nexport async function POST(req: Request) {\n  const { messages, token, model = \"gpt-3.5-turbo\" } = await req.json();\n\n  const configuration = new Configuration({ apiKey: token });\n  const openai = new OpenAIApi(configuration);\n\n  try {\n    const response = await openai.createChatCompletion({\n      model,\n      stream: true,\n      messages: [\n        {\n          role: ChatCompletionRequestMessageRoleEnum.System,\n          content: SYSTEM_PROMPT,\n        },\n        ...messages,\n      ],\n    });\n\n    if (response.status >= 300) {\n      const body = await response.json();\n      return NextResponse.json(\n        { error: `OpenAI error encountered: ${body?.error?.message}.` },\n        { status: response.status }\n      );\n    }\n\n    const stream = OpenAIStream(response);\n    return new StreamingTextResponse(stream);\n  } catch (e) {\n    console.error(e);\n\n    return NextResponse.json(\n      { error: \"An unexpected error occurred. Please try again later.\" },\n      { status: 500 }\n    );\n  }\n}\n"
  },
  {
    "path": "src/app/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n"
  },
  {
    "path": "src/app/layout.tsx",
    "content": "import { Analytics } from \"@vercel/analytics/react\";\nimport classNames from \"classnames\";\nimport { Inter, JetBrains_Mono } from \"next/font/google\";\nimport \"./globals.css\";\n\nconst sans = Inter({\n  subsets: [\"latin\"],\n  variable: \"--font-sans\",\n});\nconst monospace = JetBrains_Mono({\n  subsets: [\"latin\"],\n  variable: \"--font-mono\",\n});\n\nexport const metadata = {\n  title: \"LLM Markdown\",\n  description:\n    \"App demo for rendering rich-text (markdown) from a Large Language Model.\",\n};\n\nexport default function RootLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <html lang=\"en\">\n      <body\n        className={classNames(\n          sans.className,\n          sans.variable,\n          monospace.variable\n        )}\n      >\n        {children}\n        <Analytics />\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "src/app/markdown/page.tsx",
    "content": "\"use client\";\n\nimport { Nav } from \"@/components/nav\";\nimport {\n  MARKDOWN_TEST_MESSAGE,\n  useMarkdownProcessor,\n} from \"@/hooks/use-markdown-processor\";\n\nexport default function Chat() {\n  const content = useMarkdownProcessor(MARKDOWN_TEST_MESSAGE);\n\n  return (\n    <div className=\"px-2 lg:px-8 pb-8\">\n      <div className=\"max-w-2xl w-full mx-auto\">\n        <Nav />\n        <h1 className=\"font-sans text-2xl font-semibold text-emerald-950 mb-4 mt-8\">\n          Supported Markdown\n        </h1>\n        <p className=\"font-sans text-base text-emerald-950\">\n          Below is the markdown that is supported in the LLM responses.\n        </p>\n        <hr className=\"border-t-2 border-emerald-100 my-8\" />\n        <div className=\"[&>*:first-child]:mt-0 [&>*:last-child]:mb-0\">\n          {content}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/app/page.tsx",
    "content": "\"use client\";\n\nimport { EmptyMessage } from \"@/components/empty-message\";\nimport { MessageList } from \"@/components/message-list\";\nimport {\n  ModelDialog,\n  SUPPORTED_MODELS,\n  SupportedModels,\n} from \"@/components/model-dialog\";\nimport { Nav } from \"@/components/nav\";\nimport { TokenDialog } from \"@/components/token-dialog\";\nimport { useLocalStorage } from \"@/hooks/use-local-storage\";\nimport { ANCHOR_CLASS_NAME } from \"@/hooks/use-markdown-processor\";\nimport PaperPlaneRight from \"@phosphor-icons/react/dist/icons/PaperPlaneRight\";\nimport { useChat } from \"ai/react\";\nimport { useState } from \"react\";\n\nconst parseError = (error: Error) => {\n  try {\n    return JSON.parse(error.message).error;\n  } catch (e) {\n    console.error(e);\n    return error.message;\n  }\n};\n\nexport default function Chat() {\n  const [tokenOpen, setTokenOpen] = useState(false);\n  const [modelOpen, setModelOpen] = useState(false);\n  const [token, setToken] = useLocalStorage<string | null>(\"ai-token\", null);\n  const [model, setModel] = useLocalStorage<SupportedModels>(\n    \"ai-model\",\n    SUPPORTED_MODELS[0]\n  );\n  const { messages, input, handleInputChange, handleSubmit, error } = useChat({\n    body: { token, model },\n  });\n\n  return (\n    <>\n      <div className=\"flex flex-col-reverse h-screen overflow-y-scroll\">\n        <div className=\"mx-auto w-full px-2 lg:px-8 pb-8 flex flex-col stretch gap-8 flex-1\">\n          <Nav />\n          {messages.length ? (\n            <MessageList messages={messages} />\n          ) : (\n            <EmptyMessage />\n          )}\n\n          <form onSubmit={handleSubmit} className=\"max-w-2xl w-full mx-auto\">\n            {error ? (\n              <div className=\"p-3 rounded-lg bg-rose-100 border-2 border-rose-200 mb-3\">\n                <p className=\"font-sans text-sm text-red text-rose-800\">\n                  {parseError(error)}\n                </p>\n              </div>\n            ) : null}\n            <div className=\"relative\">\n              <input\n                className=\"w-full border-2 border-slate-200 rounded-lg p-2 font-sans text-base outline-none ring-offset-0 focus:border-slate-400 focus-visible:ring-2 focus-visible:ring-offset-2 ring-emerald-600 transition-[box-shadow,border-color] pr-10 disabled:opacity-60 disabled:cursor-not-allowed\"\n                value={input}\n                onChange={handleInputChange}\n                aria-label=\"ask a question\"\n                placeholder=\"Ask a question...\"\n                disabled={!token}\n              />\n              <button\n                type=\"submit\"\n                aria-label=\"send\"\n                className=\"absolute top-0 right-0 bottom-0 text-emerald-600 outline-none p-3 disabled:text-slate-600 disabled:opacity-60 disabled:cursor-not-allowed hover:text-emerald-800 focus:text-emerald-800 transition-colors\"\n                disabled={!input}\n              >\n                <PaperPlaneRight size=\"1em\" />\n              </button>\n            </div>\n            <div className=\"mt-3 flex gap-2 justify-between\">\n              <div className=\"flex items-center gap-1\">\n                <button\n                  type=\"button\"\n                  className={ANCHOR_CLASS_NAME}\n                  onClick={() => setModelOpen(true)}\n                >\n                  <div className=\"font-sans text-xs font-medium\">\n                    Change Model\n                  </div>\n                </button>\n                <p className=\"font-sans text-xs text-slate-500 inline-block\">\n                  ({model})\n                </p>\n              </div>\n              <button\n                type=\"button\"\n                className={ANCHOR_CLASS_NAME}\n                onClick={() => setTokenOpen(true)}\n              >\n                <div className=\"font-sans text-xs font-medium\">\n                  {token ? \"Change API Key\" : \"Set API Key\"}\n                </div>\n              </button>\n            </div>\n          </form>\n        </div>\n      </div>\n      <TokenDialog\n        // Clear input between opening/closing.\n        key={tokenOpen.toString()}\n        open={tokenOpen}\n        setOpen={setTokenOpen}\n        setToken={setToken}\n      />\n      <ModelDialog\n        open={modelOpen}\n        setOpen={setModelOpen}\n        model={model}\n        setModel={setModel}\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "src/components/assistant-message.tsx",
    "content": "import { useMarkdownProcessor } from \"@/hooks/use-markdown-processor\";\n\ninterface Props {\n  children: string;\n}\n\nexport const AssistantMessage = ({ children }: Props) => {\n  const content = useMarkdownProcessor(children);\n\n  return (\n    <li className=\"flex flex-col flex-1 min-w-0 gap-1 ml-6 selection:bg-emerald-300 selection:text-emerald-900\">\n      <p className=\"font-sans text-xs font-medium text-emerald-700\">AI:</p>\n      <div className=\"p-2 lg:p-6 border-2 border-emerald-200 rounded-lg bg-emerald-50 text-emerald-900 min-w-0 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0\">\n        {content}\n      </div>\n    </li>\n  );\n};\n"
  },
  {
    "path": "src/components/dialog.tsx",
    "content": "import X from \"@phosphor-icons/react/dist/icons/X\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { ReactNode } from \"react\";\n\ninterface Props {\n  open: boolean;\n  setOpen(open: boolean): void;\n  title: string;\n  description?: ReactNode;\n  children: ReactNode;\n  size?: \"xl\" | \"3xl\";\n}\n\nexport const Dialog = ({\n  open,\n  setOpen,\n  title,\n  description,\n  children,\n  size = \"xl\",\n}: Props) => {\n  return (\n    <DialogPrimitive.Root open={open} onOpenChange={setOpen}>\n      <DialogPrimitive.Portal>\n        <DialogPrimitive.Overlay className=\"fixed inset-0 py-8 bg-slate-900/90 grid place-items-center overflow-y-auto z-50\">\n          <DialogPrimitive.Content\n            className={`bg-white rounded-lg shadow-2xl p-8 w-[90vw] relative ${\n              size === \"3xl\" ? \"max-w-3xl\" : \"max-w-xl\"\n            }`}\n          >\n            <DialogPrimitive.Title className=\"font-sans text-lg font-semibold mb-4\">\n              {title}\n            </DialogPrimitive.Title>\n            {description ? (\n              <DialogPrimitive.Description className=\"font-sans text-sm mb-8\">\n                {description}\n              </DialogPrimitive.Description>\n            ) : null}\n            {children}\n            <DialogPrimitive.Close asChild>\n              <button\n                className=\"absolute top-6 right-6 p-2 rounded-full hover:bg-emerald-50 transition-colors\"\n                aria-label=\"Close\"\n              >\n                <X className=\"w-4 h-4 text-slate-700\" />\n              </button>\n            </DialogPrimitive.Close>\n          </DialogPrimitive.Content>\n        </DialogPrimitive.Overlay>\n      </DialogPrimitive.Portal>\n    </DialogPrimitive.Root>\n  );\n};\n"
  },
  {
    "path": "src/components/empty-message.tsx",
    "content": "import { ANCHOR_CLASS_NAME } from \"@/hooks/use-markdown-processor\";\nimport Link from \"next/link\";\n\nexport const EmptyMessage = () => {\n  return (\n    <div className=\"max-w-2xl my-auto mx-auto w-full bg-emerald-50 border-2 border-emerald-100 p-4 lg:p-8 rounded-lg text-emerald-950\">\n      <p className=\"font-sans text-base font-semibold mb-6\">\n        Welcome to LLM Markdown!\n      </p>\n      <p className=\"font-sans text-base mb-6\">\n        This app is a demo for supporting rich-text responses from a{\" \"}\n        <Link\n          href=\"https://en.wikipedia.org/wiki/Large_language_model\"\n          target=\"_blank\"\n          className={ANCHOR_CLASS_NAME}\n        >\n          Large Language Model\n        </Link>{\" \"}\n        (LLM). Once you provide your OpenAI API Key, you can start prompting and\n        receiving rich-text responses.\n      </p>\n      <p className=\"font-sans text-base mb-6\">\n        Try something like:{\" \"}\n        <em className=\"italic font-semibold\">\n          &quot;Top 10 grossing movies of all time as a pie chart&quot;\n        </em>\n      </p>\n\n      <p className=\"font-sans text-base font-semibold mt-10 mb-6\">\n        How it works\n      </p>\n\n      <p className=\"font-sans text-base mb-6\">\n        A system prompt is used to encourage the LLM to generate a response with\n        Markdown and Mermaid formatting.\n      </p>\n      <p className=\"font-sans text-base mb-6\">\n        The response is rendered using{\" \"}\n        <Link\n          href=\"https://unifiedjs.com\"\n          target=\"_blank\"\n          className={ANCHOR_CLASS_NAME}\n        >\n          unified\n        </Link>\n        ,{\" \"}\n        <Link\n          href=\"https://mermaid.js.org\"\n          target=\"_blank\"\n          className={ANCHOR_CLASS_NAME}\n        >\n          Mermaid\n        </Link>\n        , and{\" \"}\n        <Link\n          href=\"https://latex.js.org\"\n          target=\"_blank\"\n          className={ANCHOR_CLASS_NAME}\n        >\n          LaTeX.js\n        </Link>{\" \"}\n        with{\" \"}\n        <Link\n          href=\"https://nextjs.org\"\n          target=\"_blank\"\n          className={ANCHOR_CLASS_NAME}\n        >\n          Nextjs\n        </Link>\n        .\n      </p>\n      <p className=\"font-sans text-base mb-6\">\n        For an example of the supported formatting, see{\" \"}\n        <Link href=\"/markdown\" className={ANCHOR_CLASS_NAME}>\n          supported markdown\n        </Link>\n        .\n      </p>\n      <p className=\"font-sans text-base mb-6\">\n        All the source code is available on{\" \"}\n        <Link\n          href=\"https://github.com/skovy/llm-markdown\"\n          target=\"_blank\"\n          className={ANCHOR_CLASS_NAME}\n        >\n          GitHub\n        </Link>\n        .\n      </p>\n    </div>\n  );\n};\n"
  },
  {
    "path": "src/components/message-list.tsx",
    "content": "import Message from \"./message\";\nimport { Message as MessageType } from \"ai/react\";\n\ninterface Props {\n  messages: MessageType[];\n}\n\nexport const MessageList = ({ messages }: Props) => {\n  return (\n    <ul className=\"grid auto-rows-min\tgap-4 max-w-2xl flex-1 mx-auto w-full\">\n      {messages.map((m) => (\n        <Message key={m.id} message={m} />\n      ))}\n    </ul>\n  );\n};\n"
  },
  {
    "path": "src/components/message.tsx",
    "content": "import { Message } from \"ai/react\";\nimport { UserMessage } from \"./user-message\";\nimport { AssistantMessage } from \"./assistant-message\";\n\ninterface Props {\n  message: Message;\n}\n\nexport default function Message({ message }: Props) {\n  switch (message.role) {\n    case \"user\":\n      return <UserMessage>{message.content}</UserMessage>;\n    case \"assistant\":\n      return <AssistantMessage>{message.content}</AssistantMessage>;\n    default:\n      throw new Error(`Unknown message role: ${message.role}`);\n  }\n}\n"
  },
  {
    "path": "src/components/model-dialog.tsx",
    "content": "import { Dialog } from \"./dialog\";\n\nexport const SUPPORTED_MODELS = [\n  \"gpt-3.5-turbo\",\n  \"gpt-3.5-turbo-16k\",\n  \"gpt-4\",\n] as const;\n\nexport type SupportedModels = (typeof SUPPORTED_MODELS)[number];\n\ninterface Props {\n  open: boolean;\n  setOpen(open: boolean): void;\n  model: SupportedModels;\n  setModel(mode: SupportedModels): void;\n}\n\nexport const ModelDialog = ({ open, setOpen, model, setModel }: Props) => {\n  return (\n    <Dialog\n      open={open}\n      setOpen={setOpen}\n      title=\"OpenAI Model\"\n      description={\n        <>\n          Choose the OpenAI model to use. Note, you must have access to the\n          model you select. If you are not sure, select{\" \"}\n          <code className=\"font-mono text-emerald-950\">gpt-3.5-turbo</code>.\n        </>\n      }\n    >\n      <fieldset className=\"flex-grow-0\">\n        <label\n          className=\"font-sans text-sm text-emerald-950 font-semibold mb-2 block\"\n          htmlFor=\"model\"\n        >\n          Model\n        </label>\n        <select\n          id=\"model\"\n          name=\"model\"\n          value={model}\n          onChange={(e) => setModel(e.currentTarget.value as SupportedModels)}\n          className=\"w-full border-2 border-slate-200 rounded-lg p-2 font-sans text-base outline-none ring-offset-0 focus:border-slate-400 focus-visible:ring-2 focus-visible:ring-offset-2 ring-emerald-600 transition-[box-shadow,border-color]\"\n        >\n          {SUPPORTED_MODELS.map((model) => (\n            <option key={model} value={model}>\n              {model}\n            </option>\n          ))}\n        </select>\n      </fieldset>\n    </Dialog>\n  );\n};\n"
  },
  {
    "path": "src/components/nav.tsx",
    "content": "import { GithubLogo } from \"@phosphor-icons/react\";\nimport MarkerCircle from \"@phosphor-icons/react/dist/icons/MarkerCircle\";\nimport Link from \"next/link\";\n\nexport const Nav = () => {\n  return (\n    <nav className=\"max-w-2xl w-full mx-auto py-2 lg:py-4 flex gap-2 items-center justify-between sticky top-0 bg-white z-10\">\n      <a\n        href=\"/\"\n        className=\"flex gap-2 items-center text-emerald-900 hover:text-emerald-950 group transition-colors\"\n      >\n        <div className=\"p-2 rounded-full bg-emerald-50 group-hover:bg-emerald-100 transition-colors\">\n          <MarkerCircle className=\"w-6 h-6\" />\n        </div>\n        <span className=\"font-mono font-semibold\">LLM Markdown</span>\n      </a>\n      <Link\n        href=\"https://github.com/skovy/llm-markdown\"\n        target=\"_blank\"\n        aria-label=\"Open GitHub repository\"\n        className=\"p-2 rounded-full bg-slate-100 text-slate-600 hover:bg-slate-200 hover:text-slate-800 transition-colors\"\n      >\n        <GithubLogo className=\"w-6 h-6\" />\n      </Link>\n    </nav>\n  );\n};\n"
  },
  {
    "path": "src/components/token-dialog.tsx",
    "content": "import { ANCHOR_CLASS_NAME } from \"@/hooks/use-markdown-processor\";\nimport { useState } from \"react\";\nimport { Dialog } from \"./dialog\";\n\ninterface Props {\n  open: boolean;\n  setOpen(open: boolean): void;\n  setToken(key: string): void;\n}\n\nexport const TokenDialog = ({ open, setOpen, setToken }: Props) => {\n  const [tokenInput, setTokenInput] = useState(\"\");\n\n  return (\n    <Dialog\n      open={open}\n      setOpen={setOpen}\n      title=\"Set OpenAI API Key\"\n      description={\n        <>\n          Since this is a demo app, you will need to provide your own OpenAI API\n          Key. This will be saved in your browser&apos;s local storage under the\n          name{\" \"}\n          <code className=\"font-mono text-emerald-900 font-semibold\">\n            ai-token\n          </code>\n          . This will only be sent when you ask a question, and never persisted\n          outside of your browser. If you have not obtained your OpenAI API key,\n          you can do so by{\" \"}\n          <a\n            href=\"https://platform.openai.com/signup/\"\n            className={ANCHOR_CLASS_NAME}\n          >\n            signing up\n          </a>{\" \"}\n          on the OpenAI website.\n        </>\n      }\n    >\n      <fieldset>\n        <label\n          className=\"font-sans text-sm text-emerald-950 font-semibold mb-2 block\"\n          htmlFor=\"token\"\n        >\n          OpenAI API Key\n        </label>\n        <input\n          id=\"token\"\n          name=\"token\"\n          type=\"password\"\n          value={tokenInput}\n          onChange={(e) => setTokenInput(e.target.value)}\n          className=\"w-full border-2 border-slate-200 rounded-lg p-2 font-sans text-base outline-none ring-offset-0 focus:border-slate-400 focus-visible:ring-2 focus-visible:ring-offset-2 ring-emerald-600 transition-[box-shadow,border-color] pr-10\"\n        />\n      </fieldset>\n      <div className=\"flex justify-end mt-8 gap-2\">\n        <button\n          type=\"button\"\n          onClick={() => setOpen(false)}\n          className=\"p-3 text-emerald-900 border-2 border-emerald-900 rounded-lg hover:bg-emerald-50 transition-colors\"\n        >\n          <div className=\"font-sans text-base font-medium\">Cancel</div>\n        </button>\n        <button\n          type=\"button\"\n          onClick={() => {\n            setToken(tokenInput);\n            setOpen(false);\n          }}\n          disabled={!tokenInput}\n          className=\"p-3 text-emerald-50 border-2 border-emerald-900 bg-emerald-900 rounded-lg disabled:opacity-60 disabled:cursor-not-allowed hover:bg-emerald-800 transition-colors\"\n        >\n          <div className=\"font-sans text-base font-medium\">Save</div>\n        </button>\n      </div>\n    </Dialog>\n  );\n};\n"
  },
  {
    "path": "src/components/user-message.tsx",
    "content": "interface Props {\n  children: string;\n}\n\nexport const UserMessage = ({ children }: Props) => {\n  return (\n    <li className=\"flex flex-col flex-1 min-w-0 gap-1 mr-6 selection:bg-indigo-300 selection:text-indigo-900\">\n      <p className=\"font-sans text-xs font-medium text-indigo-700\">You:</p>\n      <p className=\"p-2 lg:p-6 border-2 border-indigo-200 rounded-lg bg-indigo-50 font-sans text-sm text-indigo-900 min-w-0\">\n        {children}\n      </p>\n    </li>\n  );\n};\n"
  },
  {
    "path": "src/hooks/use-local-storage.ts",
    "content": "import { useEffect, useState } from \"react\";\n\nexport const useLocalStorage = <T>(\n  key: string,\n  initialValue: T\n): [T, (value: T) => void] => {\n  const [storedValue, setStoredValue] = useState(initialValue);\n\n  useEffect(() => {\n    // Retrieve from localStorage\n    const item = window.localStorage.getItem(key);\n    if (item) {\n      setStoredValue(JSON.parse(item));\n    }\n  }, [key]);\n\n  const setValue = (value: T) => {\n    // Save state\n    setStoredValue(value);\n    // Save to localStorage\n    window.localStorage.setItem(key, JSON.stringify(value));\n  };\n\n  return [storedValue, setValue];\n};\n"
  },
  {
    "path": "src/hooks/use-markdown-processor.tsx",
    "content": "import { Dialog } from \"@/components/dialog\";\nimport { CircleNotch, MathOperations } from \"@phosphor-icons/react\";\nimport CheckFat from \"@phosphor-icons/react/dist/icons/CheckFat\";\nimport Copy from \"@phosphor-icons/react/dist/icons/Copy\";\nimport FlowArrow from \"@phosphor-icons/react/dist/icons/FlowArrow\";\nimport { Root } from \"hast\";\nimport \"highlight.js/styles/base16/green-screen.css\";\nimport mermaid from \"mermaid\";\nimport Link from \"next/link\";\nimport {\n  Children,\n  Fragment,\n  createElement,\n  isValidElement,\n  useEffect,\n  useMemo,\n  useState,\n  useRef,\n} from \"react\";\nimport flattenChildren from \"react-keyed-flatten-children\";\nimport rehypeHighlight from \"rehype-highlight\";\nimport rehypeReact from \"rehype-react\";\nimport remarkGfm from \"remark-gfm\";\nimport remarkParse from \"remark-parse\";\nimport remarkRehype from \"remark-rehype\";\nimport { Plugin, unified } from \"unified\";\nimport { visit } from \"unist-util-visit\";\n// @ts-expect-error\nimport { HtmlGenerator, parse } from \"latex.js\";\n// import \"node_modules/latex.js/dist/css/base.css\"\nimport \"node_modules/latex.js/dist/css/katex.css\";\nexport const ANCHOR_CLASS_NAME =\n  \"font-semibold underline text-emerald-700 underline-offset-[2px] decoration-1 hover:text-emerald-800 transition-colors\";\n\n// Mixing arbitrary Markdown + Capsize leads to lots of challenges\n// with paragraphs and list items. This replaces paragraphs inside\n// list items into divs to avoid nesting Capsize.\nconst rehypeListItemParagraphToDiv: Plugin<[], Root> = () => {\n  return (tree) => {\n    visit(tree, \"element\", (element) => {\n      if (element.tagName === \"li\") {\n        element.children = element.children.map((child) => {\n          if (child.type === \"element\" && child.tagName === \"p\") {\n            child.tagName = \"div\";\n          }\n          return child;\n        });\n      }\n    });\n    return tree;\n  };\n};\n\nexport const useMarkdownProcessor = (content: string) => {\n  useEffect(() => {\n    mermaid.initialize({ startOnLoad: false, theme: \"forest\" });\n  }, []);\n\n  return useMemo(() => {\n    return unified()\n      .use(remarkParse)\n      .use(remarkGfm)\n      .use(remarkRehype)\n      .use(rehypeHighlight, { ignoreMissing: true })\n      .use(rehypeListItemParagraphToDiv)\n      .use(rehypeReact, {\n        createElement,\n        Fragment,\n        components: {\n          a: ({ href, children }: JSX.IntrinsicElements[\"a\"]) => (\n            <a\n              href={href}\n              target=\"_blank\"\n              rel=\"noreferrer\"\n              className={ANCHOR_CLASS_NAME}\n            >\n              {children}\n            </a>\n          ),\n          h1: ({ children, id }: JSX.IntrinsicElements[\"h1\"]) => (\n            <h1\n              className=\"font-sans font-semibold text-2xl text-emerald-950 mb-6 mt-6\"\n              id={id}\n            >\n              {children}\n            </h1>\n          ),\n          h2: ({ children, id }: JSX.IntrinsicElements[\"h2\"]) => (\n            <h2\n              className=\"font-sans font-medium text-2xl text-emerald-950 mb-6 mt-6\"\n              id={id}\n            >\n              {children}\n            </h2>\n          ),\n          h3: ({ children, id }: JSX.IntrinsicElements[\"h3\"]) => (\n            <h3\n              className=\"font-sans font-semibold text-xl text-emerald-950 mb-6 mt-2\"\n              id={id}\n            >\n              {children}\n            </h3>\n          ),\n          h4: ({ children, id }: JSX.IntrinsicElements[\"h4\"]) => (\n            <h4\n              className=\"font-sans font-medium text-xl text-emerald-950 my-6\"\n              id={id}\n            >\n              {children}\n            </h4>\n          ),\n          h5: ({ children, id }: JSX.IntrinsicElements[\"h5\"]) => (\n            <h5\n              className=\"font-sans font-semibold text-lg text-emerald-950 my-6\"\n              id={id}\n            >\n              {children}\n            </h5>\n          ),\n          h6: ({ children, id }: JSX.IntrinsicElements[\"h6\"]) => (\n            <h6\n              className=\"font-sans font-medium text-lg text-emerald-950 my-6\"\n              id={id}\n            >\n              {children}\n            </h6>\n          ),\n          p: (props: JSX.IntrinsicElements[\"p\"]) => {\n            return (\n              <p className=\"font-sans text-sm text-emerald-900 mb-6\">\n                {props.children}\n              </p>\n            );\n          },\n          strong: ({ children }: JSX.IntrinsicElements[\"strong\"]) => (\n            <strong className=\"text-emerald-950 font-semibold\">\n              {children}\n            </strong>\n          ),\n          em: ({ children }: JSX.IntrinsicElements[\"em\"]) => (\n            <em>{children}</em>\n          ),\n          code: CodeBlock,\n          pre: ({ children }: JSX.IntrinsicElements[\"pre\"]) => {\n            return (\n              <div className=\"relative mb-6\">\n                <pre className=\"p-4 rounded-lg border-2 border-emerald-200 bg-emerald-100 [&>code.hljs]:p-0 [&>code.hljs]:bg-transparent font-code text-sm overflow-x-auto flex items-start\">\n                  {children}\n                </pre>\n              </div>\n            );\n          },\n          ul: ({ children }: JSX.IntrinsicElements[\"ul\"]) => (\n            <ul className=\"flex flex-col gap-3 text-emerald-900 my-6 pl-3 [&_ol]:my-3 [&_ul]:my-3\">\n              {Children.map(\n                flattenChildren(children).filter(isValidElement),\n                (child, index) => (\n                  <li key={index} className=\"flex gap-2 items-start\">\n                    <div className=\"w-1 h-1 rounded-full bg-current block shrink-0 mt-1\" />\n                    {child}\n                  </li>\n                )\n              )}\n            </ul>\n          ),\n          ol: ({ children }: JSX.IntrinsicElements[\"ol\"]) => (\n            <ol className=\"flex flex-col gap-3 text-emerald-900 my-6 pl-3 [&_ol]:my-3 [&_ul]:my-3\">\n              {Children.map(\n                flattenChildren(children).filter(isValidElement),\n                (child, index) => (\n                  <li key={index} className=\"flex gap-2 items-start\">\n                    <div\n                      className=\"font-sans text-sm text-emerald-900 font-semibold shrink-0 min-w-[1.4ch]\"\n                      aria-hidden\n                    >\n                      {index + 1}.\n                    </div>\n                    {child}\n                  </li>\n                )\n              )}\n            </ol>\n          ),\n          li: ({ children }: JSX.IntrinsicElements[\"li\"]) => (\n            <div className=\"font-sans text-sm\">{children}</div>\n          ),\n          table: ({ children }: JSX.IntrinsicElements[\"table\"]) => (\n            <div className=\"overflow-x-auto mb-6\">\n              <table className=\"table-auto border-2 border-emerald-200\">\n                {children}\n              </table>\n            </div>\n          ),\n          thead: ({ children }: JSX.IntrinsicElements[\"thead\"]) => (\n            <thead className=\"bg-emerald-100\">{children}</thead>\n          ),\n          th: ({ children }: JSX.IntrinsicElements[\"th\"]) => (\n            <th className=\"border-2 border-emerald-200 p-2 font-sans text-sm font-semibold text-emerald-950\">\n              {children}\n            </th>\n          ),\n          td: ({ children }: JSX.IntrinsicElements[\"td\"]) => (\n            <td className=\"border-2 border-emerald-200 p-2 font-sans text-sm text-emerald-900\">\n              {children}\n            </td>\n          ),\n          blockquote: ({ children }: JSX.IntrinsicElements[\"blockquote\"]) => (\n            <blockquote className=\"border-l-4 border-emerald-200 pl-2 text-emerald-900 italic\">\n              {children}\n            </blockquote>\n          ),\n        },\n      })\n      .processSync(content).result;\n  }, [content]);\n};\n\nconst CodeBlock = ({ children, className }: JSX.IntrinsicElements[\"code\"]) => {\n  const [copied, setCopied] = useState(false);\n  const [showMermaidPreview, setShowMermaidPreview] = useState(false);\n  const [showLatexPreview, setShowLatexPreview] = useState(false);\n  const ref = useRef<HTMLElement>(null);\n\n  useEffect(() => {\n    if (copied) {\n      const interval = setTimeout(() => setCopied(false), 1000);\n      return () => clearTimeout(interval);\n    }\n  }, [copied]);\n\n  // Highlight.js adds a `className` so this is a hack to detect if the code block\n  // is a language block wrapped in a `pre` tag.\n  if (className) {\n    const isMermaid = className.includes(\"language-mermaid\");\n    const isLatex = className.includes(\"language-latex\");\n\n    return (\n      <>\n        <code ref={ref} className={`${className} flex-grow flex-shrink my-auto`}>\n          {children}\n        </code>\n        <div className=\"flex flex-col gap-1 flex-grow-0 flex-shrink-0\">\n          <button\n            type=\"button\"\n            className=\"rounded-md p-1 text-emerald-900 hover:bg-emerald-200 border-2 border-emerald-200 transition-colors\"\n            aria-label=\"copy code to clipboard\"\n            title=\"Copy code to clipboard\"\n            onClick={() => {\n              if (ref.current) {\n                navigator.clipboard.writeText(ref.current.innerText ?? \"\");\n                setCopied(true);\n              }\n            }}\n          >\n            {copied ? (\n              <CheckFat className=\"w-4 h-4\" />\n            ) : (\n              <Copy className=\"w-4 h-4\" />\n            )}\n          </button>\n          {isMermaid ? (\n            <>\n              <button\n                type=\"button\"\n                className=\"rounded-md p-1 text-emerald-900 hover:bg-emerald-200 border-2 border-emerald-200 transition-colors\"\n                aria-label=\"Open Mermaid preview\"\n                title=\"Open Mermaid preview\"\n                onClick={() => {\n                  setShowMermaidPreview(true);\n                }}\n              >\n                <FlowArrow className=\"w-4 h-4\" />\n              </button>\n              <Dialog\n                open={showMermaidPreview}\n                setOpen={setShowMermaidPreview}\n                title=\"Mermaid diagram preview\"\n                size=\"3xl\"\n              >\n                <Mermaid content={children?.toString() ?? \"\"} />\n              </Dialog>\n            </>\n          ) : null}\n          {isLatex ? (\n            <>\n              <button\n                type=\"button\"\n                className=\"rounded-md p-1 text-emerald-900 hover:bg-emerald-200 border-2 border-emerald-200 transition-colors\"\n                aria-label=\"Open Latex preview\"\n                title=\"Open Latex preview\"\n                onClick={() => {\n                  setShowLatexPreview(true);\n                }}\n              >\n                <MathOperations className=\"w-4 h-4\" />\n              </button>\n              <Dialog\n                open={showLatexPreview}\n                setOpen={setShowLatexPreview}\n                title=\"Latex diagram preview\"\n                size=\"3xl\"\n              >\n                <Latex content={children?.toString() ?? \"\"} />\n              </Dialog>\n            </>\n          ) : null}\n        </div>\n      </>\n    );\n  }\n\n  return (\n    <code className=\"inline-block font-code bg-emerald-100 text-emerald-950 p-0.5 -my-0.5 rounded\">\n      {children}\n    </code>\n  );\n};\n\nconst Latex = ({ content }: { content: string }) => {\n  const [diagram, setDiagram] = useState<string | boolean>(true);\n\n  useEffect(() => {\n    try {\n      const generator = new HtmlGenerator({ hyphenate: false });\n      const fragment = parse(content, { generator: generator }).domFragment();\n      setDiagram(fragment.firstElementChild.outerHTML);\n    } catch (error) {\n      console.error(error);\n      setDiagram(false);\n    }\n  }, [content]);\n\n  if (diagram === true) {\n    return (\n      <div className=\"flex gap-2 items-center\">\n        <CircleNotch className=\"animate-spin w-4 h-4 text-emerald-900\" />\n        <p className=\"font-sans text-sm text-slate-700\">Rendering diagram...</p>\n      </div>\n    );\n  } else if (diagram === false) {\n    return (\n      <p className=\"font-sans text-sm text-slate-700\">\n        Unable to render this diagram.\n      </p>\n    );\n  } else {\n    return <div dangerouslySetInnerHTML={{ __html: diagram ?? \"\" }} />;\n  }\n};\n\nconst Mermaid = ({ content }: { content: string }) => {\n  const [diagram, setDiagram] = useState<string | boolean>(true);\n\n  useEffect(() => {\n    const render = async () => {\n      // Generate a random ID for mermaid to use.\n      const id = `mermaid-svg-${Math.round(Math.random() * 10000000)}`;\n\n      // Confirm the diagram is valid before rendering.\n      if (await mermaid.parse(content, { suppressErrors: true })) {\n        const { svg } = await mermaid.render(id, content);\n        setDiagram(svg);\n      } else {\n        setDiagram(false);\n      }\n    };\n    render();\n  }, [content]);\n\n  if (diagram === true) {\n    return (\n      <div className=\"flex gap-2 items-center\">\n        <CircleNotch className=\"animate-spin w-4 h-4 text-emerald-900\" />\n        <p className=\"font-sans text-sm text-slate-700\">Rendering diagram...</p>\n      </div>\n    );\n  } else if (diagram === false) {\n    return (\n      <p className=\"font-sans text-sm text-slate-700\">\n        Unable to render this diagram. Try copying it into the{\" \"}\n        <Link\n          href=\"https://mermaid.live/edit\"\n          className={ANCHOR_CLASS_NAME}\n          target=\"_blank\"\n        >\n          Mermaid Live Editor\n        </Link>\n        .\n      </p>\n    );\n  } else {\n    return <div dangerouslySetInnerHTML={{ __html: diagram ?? \"\" }} />;\n  }\n};\n\nexport const MARKDOWN_TEST_MESSAGE = `\n# Heading level 1\n\nThis is the first paragraph.\n\nThis is the second paragraph.\n\nThis is the third paragraph.\n\n## Heading level 2\n\nThis is an [anchor](https://github.com).\n\n### Heading level 3\n\nThis is **bold** and _italics_.\n\n#### Heading level 4\n\nThis is \\`inline\\` code.\n\nThis is a code block:\n\n\\`\\`\\`tsx\nconst Message = () => {\n  return <div>hi</div>;\n};\n\\`\\`\\`\n\n##### Heading level 5\n\nThis is an unordered list:\n\n- One\n- Two\n- Three, and **bold**\n\nThis is an ordered list:\n\n1. One\n1. Two\n1. Three\n\nThis is a complex list:\n\n1. **Bold**: One\n    - One\n    - Two\n    - Three\n  \n2. **Bold**: Three\n    - One\n    - Two\n    - Three\n  \n3. **Bold**: Four\n    - One\n    - Two\n    - Three\n\n###### Heading level 6\n\n> This is a blockquote.\n\nThis is a table:\n\n| Vegetable | Description |\n|-----------|-------------|\n| Carrot    | A crunchy, orange root vegetable that is rich in vitamins and minerals. It is commonly used in soups, salads, and as a snack. |\n| Broccoli  | A green vegetable with tightly packed florets that is high in fiber, vitamins, and antioxidants. It can be steamed, boiled, stir-fried, or roasted. |\n| Spinach   | A leafy green vegetable that is dense in nutrients like iron, calcium, and vitamins. It can be eaten raw in salads or cooked in various dishes. |\n| Bell Pepper | A colorful, sweet vegetable available in different colors such as red, yellow, and green. It is often used in stir-fries, salads, or stuffed recipes. |\n| Tomato    | A juicy fruit often used as a vegetable in culinary preparations. It comes in various shapes, sizes, and colors and is used in salads, sauces, and sandwiches. |\n| Cucumber   | A cool and refreshing vegetable with a high water content. It is commonly used in salads, sandwiches, or as a crunchy snack. |\n| Zucchini | A summer squash with a mild flavor and tender texture. It can be sautéed, grilled, roasted, or used in baking recipes. |\n| Cauliflower | A versatile vegetable that can be roasted, steamed, mashed, or used to make gluten-free alternatives like cauliflower rice or pizza crust. |\n| Green Beans | Long, slender pods that are low in calories and rich in vitamins. They can be steamed, stir-fried, or used in casseroles and salads. |\n| Potato | A starchy vegetable available in various varieties. It can be boiled, baked, mashed, or used in soups, fries, and many other dishes. |\n\nThis is a mermaid diagram:\n\n\\`\\`\\`mermaid\ngitGraph\n    commit\n    commit\n    branch develop\n    checkout develop\n    commit\n    commit\n    checkout main\n    merge develop\n    commit\n    commit\n\\`\\`\\`\n\n\\`\\`\\`latex\n\\\\[F(x) = \\\\int_{a}^{b} f(x) \\\\, dx\\\\]\n\\`\\`\\`\n`;\n"
  },
  {
    "path": "tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  content: [\n    \"./src/pages/**/*.{js,ts,jsx,tsx,mdx}\",\n    \"./src/hooks/**/*.{js,ts,jsx,tsx,mdx}\",\n    \"./src/components/**/*.{js,ts,jsx,tsx,mdx}\",\n    \"./src/app/**/*.{js,ts,jsx,tsx,mdx}\",\n  ],\n  theme: {\n    fontFamily: {\n      sans: [\"var(--font-sans)\", \"sans-serif\"],\n      mono: [\"var(--font-mono)\", \"monospace\"],\n    },\n    capsize: {\n      fontMetrics: {\n        sans: require(\"@capsizecss/metrics/inter\"),\n        monospace: require(\"@capsizecss/metrics/jetBrainsMono\"),\n      },\n    },\n  },\n  plugins: [require(\"@themosaad/tailwindcss-capsize\")],\n};\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  }
]