[
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\n# rust\ntarget/\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\"tauri-apps.tauri-vscode\", \"rust-lang.rust-analyzer\"]\n}\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[workspace]\nresolver = \"2\"\nmembers = [\"src-tauri\"]\n\n# https://v2.tauri.app/test/webdriver/example/#adding-tauri-to-the-cargo-project\n[profile.release]\nincremental = false\ncodegen-units = 1\npanic = \"abort\"\nopt-level = \"s\"\nlto = true"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <img width=\"180\" src=\"./public/ChatGPT.png\" alt=\"ChatGPT\">\n  <p align=\"center\">ChatGPT Desktop Application (Available on Mac, Windows, and Linux)</p>\n</p>\n\n[![ChatGPT downloads](https://img.shields.io/github/downloads/lencx/ChatGPT/total.svg?style=flat-square)](https://github.com/lencx/ChatGPT/releases)\n[![chat](https://img.shields.io/badge/chat-discord-blue?style=flat&logo=discord)](https://discord.gg/aPhCRf4zZr)\n[![twitter](https://img.shields.io/badge/follow-lencx__-blue?style=flat&logo=Twitter)](https://twitter.com/lencx_)\n[![youtube](https://img.shields.io/youtube/channel/subscribers/UC__gTZL-OZKDPic7s_6Ntgg?style=social)](https://www.youtube.com/@lencx)\n\n<a href=\"https://www.buymeacoffee.com/lencx\" target=\"_blank\"><img src=\"https://cdn.buymeacoffee.com/buttons/v2/default-blue.png\" alt=\"Buy Me A Coffee\" style=\"height: 40px !important;width: 145px !important;\" ></a>\n\n---\n\n> [!NOTE]\n> **If you want to experience a more powerful AI wrapper application, you can try the Noi (https://github.com/lencx/Noi), which is a successor to the ChatGPT desktop application concept.**\n\nThank you very much for your interest in this project. OpenAI has now released the macOS version of the application, and a Windows version will be available later ([Introducing GPT-4o and more tools to ChatGPT free users](https://openai.com/index/gpt-4o-and-more-tools-to-chatgpt-free/)). If you prefer the official application, you can stay updated with the latest information from OpenAI.\n\nIf you want to learn about or download the previous version (v1.1.0), please click [here](https://github.com/lencx/ChatGPT/tree/release-v1.1.0).\n\nI am currently looking for some differentiating features to develop version 2.0. If you are interested in this, please stay tuned.\n\n![](./docs/static/chatgpt-v2.gif)"
  },
  {
    "path": "index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>ChatGPT</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"chatgpt\",\n  \"private\": true,\n  \"version\": \"2.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"preview\": \"vite preview\",\n    \"tauri\": \"tauri\"\n  },\n  \"dependencies\": {\n    \"@tauri-apps/api\": \"2.0.0-beta.15\",\n    \"@tauri-apps/plugin-os\": \"2.0.0-beta.7\",\n    \"@tauri-apps/plugin-shell\": \"2.0.0-beta.8\",\n    \"clsx\": \"^2.1.1\",\n    \"lodash\": \"^4.17.21\",\n    \"react\": \"^18.3.1\",\n    \"react-dom\": \"^18.3.1\",\n    \"react-hotkeys-hook\": \"^4.5.0\",\n    \"react-router-dom\": \"^6.23.0\"\n  },\n  \"devDependencies\": {\n    \"@tailwindcss/typography\": \"^0.5.13\",\n    \"@tauri-apps/cli\": \"2.0.0-beta.22\",\n    \"@types/lodash\": \"^4.17.1\",\n    \"@types/node\": \"^20.12.12\",\n    \"@types/react\": \"^18.3.1\",\n    \"@types/react-dom\": \"^18.3.0\",\n    \"@vitejs/plugin-react\": \"^4.2.1\",\n    \"autoprefixer\": \"^10.4.19\",\n    \"postcss\": \"^8.4.38\",\n    \"tailwindcss\": \"^3.4.3\",\n    \"typescript\": \"^5.4.5\",\n    \"vite\": \"^5.2.11\",\n    \"vite-tsconfig-paths\": \"^4.3.2\"\n  }\n}\n"
  },
  {
    "path": "postcss.config.js",
    "content": "export default {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}"
  },
  {
    "path": "rustfmt.toml",
    "content": "max_width = 100\nhard_tabs = false\ntab_spaces = 4\nnewline_style = \"Auto\"\nuse_small_heuristics = \"Default\"\nreorder_imports = true\nreorder_modules = true\nremove_nested_parens = true\nedition = \"2021\"\nmerge_derives = true\nuse_try_shorthand = false\nuse_field_init_shorthand = false\nforce_explicit_abi = true\n# imports_granularity = \"Crate\""
  },
  {
    "path": "src/App.tsx",
    "content": "import { getCurrentWebview } from '@tauri-apps/api/webview';\n\nimport Titlebar from '~view/Titlebar';\nimport Ask from '~view/Ask';\nimport Settings from '~view/Settings';\n\nconst viewMap = {\n  titlebar: <Titlebar />,\n  ask: <Ask />,\n  settings: <Settings />,\n};\n\nexport default function App() {\n  const webview = getCurrentWebview();\n  return viewMap[webview.label as keyof typeof viewMap] || null;\n}\n"
  },
  {
    "path": "src/base.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\nhtml, body, #root {\n  margin: 0;\n  bottom: 0;\n  height: 100%;\n  overflow: hidden;\n}"
  },
  {
    "path": "src/components/WinTitlebar.tsx",
    "content": "import { useEffect, useState } from 'react';\nimport { Window } from '@tauri-apps/api/window';\n\nimport WindowClose from '~icons/WindowClose';\nimport WindowMaximize from '~icons/WindowMaximize';\nimport WindowMinimize from '~icons/WindowMinimize';\nimport WindowRestore from '~/icons/WindowRestore';\n\nexport default function WinTitlebar() {\n  const [isMax, setMax] = useState(false);\n\n  useEffect(() => {\n    (async () => {\n      const win = Window.getByLabel('core');\n      const max = await win?.isMaximized();\n      setMax(!!max);\n    })()\n  }, [])\n\n  const handleToggle = async () => {\n    const win = Window.getByLabel('core');\n    await win?.toggleMaximize();\n    setMax(!isMax);\n  }\n\n  const handleMinimize = () => {\n    const win = Window.getByLabel('core');\n    win?.minimize();\n  };\n\n  const handleClose = () => {\n    const win = Window.getByLabel('core');\n    win?.close();\n  }\n\n  return (\n    <div className=\"flex items-center gap-1\">\n      <WindowMinimize size={20} className=\"p-0\" onClick={handleMinimize} />\n      {isMax\n        ? <WindowRestore size={20} className=\"p-0\" onClick={handleToggle} />\n        : <WindowMaximize size={20} className=\"p-0\" onClick={handleToggle} />}\n      <WindowClose size={20} className=\"p-0\" onClick={handleClose} />\n    </div>\n  )\n}"
  },
  {
    "path": "src/hooks/useInfo.tsx",
    "content": "import { useEffect, useState } from 'react';\nimport { platform as TauriPlatform } from '@tauri-apps/plugin-os';\n\nexport default function useInfo() {\n  const [platform, setPlatform] = useState('');\n  const [isMac, setMac] = useState(false);\n\n  const handleInfo = async () => {\n    const p = await TauriPlatform();\n    setPlatform(await TauriPlatform());\n    setMac(p === 'macos');\n  }\n\n  useEffect(() => {\n    handleInfo();\n  }, []);\n\n  return { platform, isMac };\n}"
  },
  {
    "path": "src/hooks/useTheme.tsx",
    "content": "import { useState, useEffect } from 'react';\nimport { getCurrentWindow } from '@tauri-apps/api/window';\n\nexport default function useTheme() {\n  const [theme, setTheme] = useState<string | null>('light'); // ['light', 'dark']\n\n  useEffect(() => {\n    let unlisten: Function;\n    (async () => {\n      let win = getCurrentWindow();\n      setTheme(await win.theme() || '');\n      unlisten = await win.onThemeChanged(({ payload: newTheme }) => {\n        setTheme(newTheme);\n      });\n    })()\n\n    return () => {\n      unlisten?.();\n    };\n  }, [])\n\n  return theme;\n}"
  },
  {
    "path": "src/icons/ArrowLeft.tsx",
    "content": "import SVGWrap from './SVGWrap';\n\nexport default function ArrowLeft(props: I.SVG) {\n  return (\n    <SVGWrap {...props} viewBox=\"0 0 24 24\">\n      <path fill=\"currentColor\" d=\"m7.85 13l2.85 2.85q.3.3.288.7t-.288.7q-.3.3-.712.313t-.713-.288L4.7 12.7q-.3-.3-.3-.7t.3-.7l4.575-4.575q.3-.3.713-.287t.712.312q.275.3.288.7t-.288.7L7.85 11H19q.425 0 .713.288T20 12t-.288.713T19 13z\"/>\n    </SVGWrap>\n  );\n}\n"
  },
  {
    "path": "src/icons/Ask.tsx",
    "content": "import SVGWrap from './SVGWrap';\n\nexport default function Ask(props: I.SVG) {\n  return (\n    <SVGWrap {...props} viewBox=\"0 0 24 24\">\n      <path fill=\"none\" stroke=\"currentColor\" strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth=\"2\" d=\"M13.038 19.927A9.93 9.93 0 0 1 7.7 19L3 20l1.3-3.9C1.976 12.663 2.874 8.228 6.4 5.726c3.526-2.501 8.59-2.296 11.845.48c1.993 1.7 2.93 4.043 2.746 6.346M19 16l-2 3h4l-2 3\"/>\n    </SVGWrap>\n  );\n}\n"
  },
  {
    "path": "src/icons/Link.tsx",
    "content": "import SVGWrap from './SVGWrap';\n\nexport default function Link(props: I.SVG) {\n  return (\n    <SVGWrap {...props} viewBox=\"0 0 256 256\">\n      <path fill=\"currentColor\" d=\"M117.18 188.74a12 12 0 0 1 0 17l-5.12 5.12A58.26 58.26 0 0 1 70.6 228a58.62 58.62 0 0 1-41.46-100.08l34.75-34.75a58.64 58.64 0 0 1 98.56 28.11a12 12 0 1 1-23.37 5.44a34.65 34.65 0 0 0-58.22-16.58l-34.75 34.75A34.62 34.62 0 0 0 70.57 204a34.41 34.41 0 0 0 24.49-10.14l5.11-5.12a12 12 0 0 1 17.01 0M226.83 45.17a58.65 58.65 0 0 0-82.93 0l-5.11 5.11a12 12 0 0 0 17 17l5.12-5.12a34.63 34.63 0 1 1 49 49l-34.81 34.7A34.39 34.39 0 0 1 150.61 156a34.63 34.63 0 0 1-33.69-26.72a12 12 0 0 0-23.38 5.44A58.64 58.64 0 0 0 150.56 180h.05a58.28 58.28 0 0 0 41.47-17.17l34.75-34.75a58.62 58.62 0 0 0 0-82.91\"/>\n    </SVGWrap>\n  );\n}\n"
  },
  {
    "path": "src/icons/Pin.tsx",
    "content": "import SVGWrap from './SVGWrap';\n\nexport default function Pin(props: I.SVG) {\n  return (\n    <SVGWrap {...props} viewBox=\"0 0 24 24\">\n      <path fill=\"currentColor\" fillRule=\"evenodd\" d=\"M16 9V4h1c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1h1v5c0 1.66-1.34 3-3 3v2h5.97v7l1 1l1-1v-7H19v-2c-1.66 0-3-1.34-3-3\"/>\n    </SVGWrap>\n  );\n}\n"
  },
  {
    "path": "src/icons/Reload.tsx",
    "content": "import SVGWrap from './SVGWrap';\n\nexport default function Reload(props: I.SVG) {\n  return (\n    <SVGWrap {...props} viewBox=\"0 0 24 24\">\n      <path fill=\"currentColor\" d=\"M12 20q-3.35 0-5.675-2.325T4 12t2.325-5.675T12 4q1.725 0 3.3.712T18 6.75V5q0-.425.288-.712T19 4t.713.288T20 5v5q0 .425-.288.713T19 11h-5q-.425 0-.712-.288T13 10t.288-.712T14 9h3.2q-.8-1.4-2.187-2.2T12 6Q9.5 6 7.75 7.75T6 12t1.75 4.25T12 18q1.7 0 3.113-.862t2.187-2.313q.2-.35.563-.487t.737-.013q.4.125.575.525t-.025.75q-1.025 2-2.925 3.2T12 20\"/>\n    </SVGWrap>\n  );\n}\n"
  },
  {
    "path": "src/icons/SVGWrap.tsx",
    "content": "import React from 'react';\nimport clsx from 'clsx';\n\nexport default function SVGWrap({ size = 18, children, type, className, title, onClick, action = false, ...props }: I.SVG) {\n  const handleClick = (e: React.MouseEvent) => {\n    onClick && onClick(e);\n  };\n\n  return (\n    <i\n      style={{\n        width: size,\n        height: size,\n      }}\n      title={title}\n      onClick={handleClick}\n      className={clsx('inline-flex items-center justify-center rounded-sm p-[2px] text-slate-500 dark:text-slate-500 transition-all', {\n        'cursor-pointer hover:bg-slate-300/50 hover:dark:bg-white/10': action,\n      }, className)}\n    >\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        style={{ widows: '100%', height: '100%' }}\n        {...props}\n      >\n        {children}\n      </svg>\n    </i>\n  );\n}\n"
  },
  {
    "path": "src/icons/Send.tsx",
    "content": "import SVGWrap from './SVGWrap';\n\nexport default function Send(props: I.SVG) {\n  return (\n    <SVGWrap {...props} viewBox=\"0 0 24 24\">\n      <g fill=\"none\">\n        <path d=\"M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022m-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z\"/>\n        <path fill=\"currentColor\" d=\"m21.433 4.861l-6 15.5a1 1 0 0 1-1.624.362l-3.382-3.235l-2.074 2.073a.5.5 0 0 1-.853-.354v-4.519L2.309 9.723a1 1 0 0 1 .442-1.691l17.5-4.5a1 1 0 0 1 1.181 1.329ZM19 6.001L8.032 13.152l1.735 1.66L19 6Z\"/>\n      </g>\n    </SVGWrap>\n  );\n}\n"
  },
  {
    "path": "src/icons/Setting.tsx",
    "content": "import SVGWrap from './SVGWrap';\n\nexport default function Setting(props: I.SVG) {\n  return (\n    <SVGWrap {...props} viewBox=\"0 0 24 24\">\n      <g fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n        <path d=\"M3.082 13.945c-.529-.95-.793-1.426-.793-1.945c0-.519.264-.994.793-1.944L4.43 7.63l1.426-2.381c.559-.933.838-1.4 1.287-1.66c.45-.259.993-.267 2.08-.285L12 3.26l2.775.044c1.088.018 1.631.026 2.08.286c.45.26.73.726 1.288 1.659L19.57 7.63l1.35 2.426c.528.95.792 1.425.792 1.944c0 .519-.264.994-.793 1.944L19.57 16.37l-1.426 2.381c-.559.933-.838 1.4-1.287 1.66c-.45.259-.993.267-2.08.285L12 20.74l-2.775-.044c-1.088-.018-1.631-.026-2.08-.286c-.45-.26-.73-.726-1.288-1.659L4.43 16.37z\"/>\n        <circle cx=\"12\" cy=\"12\" r=\"3\"/>\n      </g>\n    </SVGWrap>\n  );\n}\n"
  },
  {
    "path": "src/icons/ThemeDark.tsx",
    "content": "import SVGWrap from './SVGWrap';\n\nexport default function ThemeDark(props: I.SVG) {\n  return (\n    <SVGWrap {...props} viewBox=\"0 0 24 24\">\n      <path fill=\"currentColor\" d=\"M12 21q-3.775 0-6.387-2.613T3 12q0-3.45 2.25-5.988T11 3.05q.325-.05.575.088t.4.362t.163.525t-.188.575q-.425.65-.638 1.375T11.1 7.5q0 2.25 1.575 3.825T16.5 12.9q.775 0 1.538-.225t1.362-.625q.275-.175.563-.162t.512.137q.25.125.388.375t.087.6q-.35 3.45-2.937 5.725T12 21m0-2q2.2 0 3.95-1.213t2.55-3.162q-.5.125-1 .2t-1 .075q-3.075 0-5.238-2.163T9.1 7.5q0-.5.075-1t.2-1q-1.95.8-3.163 2.55T5 12q0 2.9 2.05 4.95T12 19m-.25-6.75\"/>\n    </SVGWrap>\n  );\n}\n"
  },
  {
    "path": "src/icons/ThemeLight.tsx",
    "content": "import SVGWrap from './SVGWrap';\n\nexport default function ThemeLight(props: I.SVG) {\n  return (\n    <SVGWrap {...props} viewBox=\"0 0 24 24\">\n      <path fill=\"currentColor\" d=\"M12 15q1.25 0 2.125-.875T15 12t-.875-2.125T12 9t-2.125.875T9 12t.875 2.125T12 15m0 2q-2.075 0-3.537-1.463T7 12t1.463-3.537T12 7t3.538 1.463T17 12t-1.463 3.538T12 17M2 13q-.425 0-.712-.288T1 12t.288-.712T2 11h2q.425 0 .713.288T5 12t-.288.713T4 13zm18 0q-.425 0-.712-.288T19 12t.288-.712T20 11h2q.425 0 .713.288T23 12t-.288.713T22 13zm-8-8q-.425 0-.712-.288T11 4V2q0-.425.288-.712T12 1t.713.288T13 2v2q0 .425-.288.713T12 5m0 18q-.425 0-.712-.288T11 22v-2q0-.425.288-.712T12 19t.713.288T13 20v2q0 .425-.288.713T12 23M5.65 7.05L4.575 6q-.3-.275-.288-.7t.288-.725q.3-.3.725-.3t.7.3L7.05 5.65q.275.3.275.7t-.275.7t-.687.288t-.713-.288M18 19.425l-1.05-1.075q-.275-.3-.275-.712t.275-.688q.275-.3.688-.287t.712.287L19.425 18q.3.275.288.7t-.288.725q-.3.3-.725.3t-.7-.3M16.95 7.05q-.3-.275-.288-.687t.288-.713L18 4.575q.275-.3.7-.288t.725.288q.3.3.3.725t-.3.7L18.35 7.05q-.3.275-.7.275t-.7-.275M4.575 19.425q-.3-.3-.3-.725t.3-.7l1.075-1.05q.3-.275.712-.275t.688.275q.3.275.288.688t-.288.712L6 19.425q-.275.3-.7.288t-.725-.288M12 12\"/>\n    </SVGWrap>\n  );\n}\n"
  },
  {
    "path": "src/icons/ThemeSystem.tsx",
    "content": "import SVGWrap from './SVGWrap';\n\nexport default function Ask(props: I.SVG) {\n  return (\n    <SVGWrap {...props} viewBox=\"0 0 24 24\">\n      <path fill=\"currentColor\" d=\"M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2S2 6.477 2 12s4.477 10 10 10m0-2V4a8 8 0 1 1 0 16\"/>\n    </SVGWrap>\n  );\n}\n"
  },
  {
    "path": "src/icons/UnPin.tsx",
    "content": "import SVGWrap from './SVGWrap';\n\nexport default function UnPin(props: I.SVG) {\n  return (\n    <SVGWrap {...props} viewBox=\"0 0 24 24\">\n      <path fill=\"currentColor\" d=\"M14 4v5c0 1.12.37 2.16 1 3H9c.65-.86 1-1.9 1-3V4zm3-2H7c-.55 0-1 .45-1 1s.45 1 1 1h1v5c0 1.66-1.34 3-3 3v2h5.97v7l1 1l1-1v-7H19v-2c-1.66 0-3-1.34-3-3V4h1c.55 0 1-.45 1-1s-.45-1-1-1\"/>\n    </SVGWrap>\n  );\n}\n"
  },
  {
    "path": "src/icons/WindowClose.tsx",
    "content": "import SVGWrap from './SVGWrap';\n\nexport default function WindowClose(props: I.SVG) {\n  return (\n    <SVGWrap {...props} viewBox=\"0 0 16 16\">\n      <path fill=\"currentColor\" d=\"m12.96 4.46l-1.42-1.42L8 6.59L4.46 3.04L3.04 4.46L6.59 8l-3.55 3.54l1.42 1.42L8 9.41l3.54 3.55l1.42-1.42L9.41 8z\"/>\n    </SVGWrap>\n  );\n}\n"
  },
  {
    "path": "src/icons/WindowMaximize.tsx",
    "content": "import SVGWrap from './SVGWrap';\n\nexport default function WindowMaximize(props: I.SVG) {\n  return (\n    <SVGWrap {...props} viewBox=\"0 0 24 24\">\n      <path fill=\"currentColor\" d=\"M4 4h16v16H4zm2 4v10h12V8z\"/>\n    </SVGWrap>\n  );\n}\n"
  },
  {
    "path": "src/icons/WindowMinimize.tsx",
    "content": "import SVGWrap from './SVGWrap';\n\nexport default function WindowMinimize(props: I.SVG) {\n  return (\n    <SVGWrap {...props} viewBox=\"0 0 24 24\">\n      <path fill=\"currentColor\" d=\"M20 14H4v-4h16\"/>\n    </SVGWrap>\n  );\n}\n"
  },
  {
    "path": "src/icons/WindowRestore.tsx",
    "content": "import SVGWrap from './SVGWrap';\n\nexport default function WindowRestore(props: I.SVG) {\n  return (\n    <SVGWrap {...props} viewBox=\"0 0 24 24\">\n      <path fill=\"currentColor\" d=\"M4 8h4V4h12v12h-4v4H4zm12 0v6h2V6h-8v2zM6 12v6h8v-6z\"/>\n    </SVGWrap>\n  );\n}\n"
  },
  {
    "path": "src/main.tsx",
    "content": "import { StrictMode } from 'react';\nimport ReactDOM from 'react-dom/client';\n\n// import Routes from './routes';\nimport App from './App';\nimport './base.css';\n\nReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(\n  <StrictMode>\n    <App />\n  </StrictMode>\n);\n"
  },
  {
    "path": "src/types.d.ts",
    "content": "declare namespace I {\n  export type AppConf = {\n    theme: 'light' | 'dark' | 'system';\n    stay_on_top: boolean;\n    ask_mode: boolean;\n    mac_titlebar_hidden: boolean;\n  }\n\n  export interface SVG extends React.SVGProps<SVGSVGElement> {\n    children?: React.ReactNode;\n    size?: number;\n    title?: string;\n    action?: boolean;\n    onClick?: (e: React.MouseEvent) => void;\n  }\n}"
  },
  {
    "path": "src/view/Ask.tsx",
    "content": "import { useState, useEffect, useRef } from 'react';\nimport { invoke } from '@tauri-apps/api/core';\nimport { useHotkeys } from 'react-hotkeys-hook';\nimport useInfo from '~hooks/useInfo';\nimport SendIcon from '~icons/Send';\nimport debounce from 'lodash/debounce';\n\nexport default function ChatInput() {\n  const inputRef = useRef<HTMLTextAreaElement>(null);\n  const [message, setMessage] = useState('');\n  const { isMac } = useInfo();\n\n  useEffect(() => {\n    const syncMessage = debounce(async () => {\n      try {\n        await invoke('ask_sync', { message: JSON.stringify(message) });\n      } catch (error) {\n        console.error('Error syncing message:', error);\n      }\n    }, 300); // Debounce by 300ms\n\n    syncMessage();\n    return () => syncMessage.cancel(); // Cleanup debounce on unmount\n  }, [message]);\n\n  useHotkeys(isMac ? 'meta+enter' : 'ctrl+enter', async (event: KeyboardEvent) => {\n    event.preventDefault();\n    await handleSend();\n  }, {\n    enableOnFormTags: true,\n  }, [message]);\n\n  const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n    setMessage(e.target.value);\n  };\n\n  const handleSend = async () => {\n    if (!message) return;\n    try {\n      await invoke('ask_send', { message: JSON.stringify(message) });\n    } catch (error) {\n      console.error('Error sending message:', error);\n    }\n    setMessage('');\n    if (inputRef.current) {\n      inputRef.current.value = '';\n      inputRef.current.focus();\n    }\n  };\n\n  return (\n    <div className=\"relative flex h-full dark:bg-app-gray-2/[0.98] bg-gray-100 dark:text-slate-200 items-center gap-1\">\n      <textarea\n        ref={inputRef}\n        onChange={handleInput}\n        spellCheck=\"false\"\n        autoFocus\n        className=\"w-full h-full pl-3 pr-[40px] py-2 outline-none resize-none bg-transparent\"\n        placeholder=\"Type your message here...\"\n      />\n      <SendIcon\n        size={30}\n        className=\"absolute right-2 text-gray-400/80 dark:text-gray-600 cursor-pointer\"\n        onClick={handleSend}\n        title={`Send message (${isMac ? '⌘⏎' : '⌃⏎'})`}\n        aria-label=\"Send message\"\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "src/view/Settings.tsx",
    "content": "export default function Settings() {\n  return (\n    <div>Settings</div>\n  )\n}"
  },
  {
    "path": "src/view/Titlebar.tsx",
    "content": "import { useEffect, useState, useMemo } from 'react';\nimport { invoke } from '@tauri-apps/api/core';\nimport { getCurrentWindow } from '@tauri-apps/api/window';\nimport { open } from '@tauri-apps/plugin-shell';\nimport { debounce } from 'lodash';\nimport clsx from 'clsx';\n\nimport useInfo from '~hooks/useInfo';\nimport ReloadIcon from '~icons/Reload';\nimport PinIcon from '~icons/Pin';\nimport UnPinIcon from '~icons/UnPin';\nimport LinkIcon from '~icons/Link';\nimport AskIcon from '~icons/Ask';\nimport SettingIcon from '~icons/Setting';\nimport ThemeSystem from '~icons/ThemeSystem';\nimport ThemeLight from '~icons/ThemeLight';\nimport ThemeDark from '~icons/ThemeDark';\nimport ArrowLeftIcon from '~icons/ArrowLeft';\n\nexport default function Titlebar() {\n  const info = useInfo();\n  const [url, setUrl] = useState('');\n  const [hostname, setHostname] = useState('');\n  const [theme, setTheme] = useState('system');\n  const [enableAsk, setEnableAsk] = useState(false);\n  const [fullScreen, setFullScreen] = useState(false);\n  const [isPin, setPin] = useState(false);\n  const [isTitlebarHidden, setTitlebarHidden] = useState(false);\n\n  const titlebarHidden = info.isMac && isTitlebarHidden;\n\n  useEffect(() => {\n    const win = getCurrentWindow();\n    let winResize: Function;\n    let changeUrl: Function;\n\n    invoke<I.AppConf>('get_app_conf')\n      .then((v) => {\n        setEnableAsk(v.ask_mode);\n        setPin(v.stay_on_top);\n        setTheme(v.theme);\n        setTitlebarHidden(v.mac_titlebar_hidden);\n      });\n\n    (async () => {\n      const full = await win.isFullscreen();\n      setFullScreen(full);\n      winResize = await win.listen('tauri://resize', debounce(async () => {\n        const full = await win.isFullscreen();\n        setFullScreen(full);\n      }, 50))\n\n      changeUrl = await getCurrentWindow().listen('navigation:change', (event: any) => {\n        const { url } = event.payload;\n        setUrl(url);\n\n        try {\n          const { hostname } = new URL(url);\n          setHostname(hostname);\n        } catch (error) {\n          setHostname(url);\n        }\n      })\n    })();\n\n    return () => {\n      winResize && winResize();\n      changeUrl && changeUrl();\n    }\n  }, [])\n\n  const handleRefresh = () => {\n    invoke('view_reload');\n  };\n\n  const handleGoForward = () => {\n    invoke('view_go_forward');\n  };\n\n  const handleGoBack = () => {\n    invoke('view_go_back');\n  };\n\n  const handlePin = (isPin: boolean) => {\n    setPin(isPin);\n    invoke('window_pin', { pin: isPin });\n  };\n\n  const handleAsk = () => {\n    setEnableAsk(!enableAsk);\n    invoke('set_view_ask', { enabled: !enableAsk });\n  };\n\n  const handleTheme = (theme: string) => {\n    invoke('set_theme', { theme });\n  };\n\n  const themeIcon = useMemo(() => {\n    switch (theme) {\n      case 'system':\n        return <ThemeSystem title=\"Light\" action onClick={() => handleTheme('light')} />\n      case 'light':\n        return <ThemeLight title=\"Dark\" action onClick={() => handleTheme('dark')} />\n      case 'dark':\n        return <ThemeDark title=\"System\" action onClick={() => handleTheme('system')} />\n      default:\n        return <ThemeSystem title=\"System\" action onClick={() => handleTheme('system')} />\n    }\n  }, [theme]);\n\n  const handleOpenUrl = () => {\n    open(url);\n  };\n\n  const handleSetting = () => {\n    invoke('open_settings');\n  };\n\n  const renderSettings = useMemo(() => {\n    return (\n      <div className={clsx('items-center gap-1', {\n        'hidden group-hover:flex': titlebarHidden,\n        'flex': !titlebarHidden,\n      })}>\n        {themeIcon}\n        {isPin\n          ? <PinIcon action onClick={() => handlePin(false)} />\n          : <UnPinIcon action onClick={() => handlePin(true)} />}\n        <SettingIcon action onClick={handleSetting} />\n      </div>\n    )\n  }, [titlebarHidden, themeIcon, isPin])\n\n  return (\n    <div data-tauri-drag-region className={clsx('flex group pr-2 h-full cursor-default select-none dark:bg-app-gray-2 justify-between', {\n      'pl-[80px]': !fullScreen && info.isMac,\n      'pl-[10px]': fullScreen || !info.isMac,\n    })}>\n      <div data-tauri-drag-region className={clsx('items-center gap-0.5', {\n        'hidden tablet:group-hover:flex group-hover:hidden': titlebarHidden,\n        'tablet:flex hidden': !titlebarHidden,\n      })}>\n        {hostname && (\n          <span\n            className=\"flex items-center bg-slate-300/50 dark:bg-slate-100/10 dark:text-gray-500 rounded-full pl-[4px] pr-[8px] h-[14px] text-[10px] gap-1 text-slate-700 mr-1\"\n            onClick={handleOpenUrl}\n            title={url}\n          >\n            <LinkIcon size={14} />\n            {hostname}\n          </span>\n        )}\n        <ArrowLeftIcon\n          action\n          onClick={handleGoBack}\n        />\n        <ArrowLeftIcon\n          action\n          onClick={handleGoForward}\n          className=\"rotate-180\"\n        />\n        <ReloadIcon action onClick={handleRefresh} />\n        <AskIcon\n          action\n          onClick={handleAsk}\n          className={clsx({\n            '!text-app-active': enableAsk,\n          })}\n        />\n      </div>\n      <div className={clsx({\n        'hidden group-hover:flex': titlebarHidden,\n        'flex': !titlebarHidden,\n      })} />\n      {renderSettings}\n    </div>\n  );\n}"
  },
  {
    "path": "src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "src-tauri/.gitignore",
    "content": "# Generated by Cargo\n# will have compiled files and executables\n/target/\n\n# Generated by Tauri\n# will have schema files for capabilities auto-completion\n/gen/schemas\n"
  },
  {
    "path": "src-tauri/Cargo.toml",
    "content": "[package]\nname = \"chatgpt\"\nversion = \"0.0.0\"\ndescription = \"ChatGPT Desktop Application (Unofficial)\"\nauthors = [\"lencx <cxin1314@gmail.com>\"]\nrepository = \"https://github.com/lencx/ChatGPT\"\nlicense = \"AGPL-3.0\"\nedition = \"2021\"\nrust-version = \"1.77.1\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[build-dependencies]\ntauri-build = { version = \"2.0.0-beta\", features = [] }\n\n[dependencies]\ntauri = { version = \"2.0.0-beta\", features = [\"unstable\", \"devtools\"] }\ntokio = { version = \"1.37.0\", features = [\"macros\"] }\ntauri-plugin-shell = \"2.0.0-beta\"\ntauri-plugin-dialog = \"2.0.0-beta\"\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\nonce_cell = \"1.19.0\"\nlog = \"0.4.21\"\nanyhow = \"1.0.83\"\ndark-light = \"1.1.1\"\nregex = \"1.10.4\"\nsemver = \"1.0.23\"\ntauri-plugin-os = \"2.0.0-beta.4\"\n"
  },
  {
    "path": "src-tauri/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>NSAppTransportSecurity</key>\n    <dict>\n      <key>NSExceptionDomains</key>\n      <dict>\n        <key>chatgpt.com</key>\n        <dict>\n          <key>NSExceptionAllowsInsecureHTTPLoads</key>\n          <true />\n          <key>NSIncludesSubdomains</key>\n          <true />\n        </dict>\n      </dict>\n    </dict>\n  </dict>\n</plist>"
  },
  {
    "path": "src-tauri/build.rs",
    "content": "// src-tauri/build.rs\n\nfn main() {\n    println!(\"cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.13\");\n    tauri_build::build()\n}\n"
  },
  {
    "path": "src-tauri/capabilities/desktop.json",
    "content": "{\n  \"$schema\": \"../gen/schemas/desktop-schema.json\",\n  \"identifier\": \"desktop-capability\",\n  \"windows\": [\n    \"*\"\n  ],\n  \"remote\": {\n    \"urls\": [\n      \"https://chatgpt.com/*\"\n    ]\n  },\n  \"platforms\": [\n    \"linux\",\n    \"macOS\",\n    \"windows\"\n  ],\n  \"permissions\": [\n    \"window:default\",\n    \"window:allow-create\",\n    \"window:allow-start-dragging\",\n    \"window:allow-toggle-maximize\",\n    \"window:allow-minimize\",\n    \"window:allow-close\",\n    \"webview:default\",\n    \"webview:allow-internal-toggle-devtools\",\n    \"webview:allow-set-webview-zoom\",\n    \"webview:allow-create-webview\",\n    \"webview:allow-create-webview-window\",\n    \"webview:allow-set-webview-focus\",\n    \"event:default\",\n    \"event:allow-emit\",\n    \"event:allow-emit-to\",\n    \"event:allow-listen\",\n    \"event:allow-unlisten\",\n    \"shell:default\",\n    \"shell:allow-execute\",\n    \"shell:allow-open\",\n    \"os:allow-arch\",\n    \"os:allow-platform\",\n    \"os:allow-version\",\n    \"os:allow-os-type\",\n    \"os:default\",\n    \"shell:default\"\n  ]\n}"
  },
  {
    "path": "src-tauri/scripts/ask.js",
    "content": "/**\n * @name ask.js\n * @version 0.1.0\n * @url https://github.com/lencx/ChatGPT/tree/main/scripts/ask.js\n */\n\nclass ChatAsk {\n  static sync(message) {\n    const inputElement = document.querySelector('textarea');\n    if (inputElement) {\n      const nativeTextareaSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;\n      nativeTextareaSetter.call(inputElement, message);\n      const inputEvent = new InputEvent('input', {\n        bubbles: true,\n        cancelable: true,\n      });\n      inputElement.dispatchEvent(inputEvent);\n    }\n  }\n\n  static submit() {\n    const btns = document.querySelectorAll('main form button');\n    const btn = btns[btns.length - 1];\n\n    if (btn) {\n      btn.focus();\n      btn.disabled = false;\n      btn.click();\n    }\n  }\n}\n\nwindow.ChatAsk = ChatAsk;"
  },
  {
    "path": "src-tauri/src/core/cmd.rs",
    "content": "use tauri::{command, AppHandle, LogicalPosition, Manager, PhysicalSize};\n\nuse crate::core::{\n    conf::AppConf,\n    constant::{ASK_HEIGHT, TITLEBAR_HEIGHT},\n};\n\n#[command]\npub fn view_reload(app: AppHandle) {\n    app.get_window(\"core\")\n        .unwrap()\n        .get_webview(\"main\")\n        .unwrap()\n        .eval(\"window.location.reload()\")\n        .unwrap();\n}\n\n#[command]\npub fn view_url(app: AppHandle) -> tauri::Url {\n    app.get_window(\"core\")\n        .unwrap()\n        .get_webview(\"main\")\n        .unwrap()\n        .url()\n        .unwrap()\n}\n\n#[command]\npub fn view_go_forward(app: AppHandle) {\n    app.get_window(\"core\")\n        .unwrap()\n        .get_webview(\"main\")\n        .unwrap()\n        .eval(\"window.history.forward()\")\n        .unwrap();\n}\n\n#[command]\npub fn view_go_back(app: AppHandle) {\n    app.get_window(\"core\")\n        .unwrap()\n        .get_webview(\"main\")\n        .unwrap()\n        .eval(\"window.history.back()\")\n        .unwrap();\n}\n\n#[command]\npub fn window_pin(app: AppHandle, pin: bool) {\n    let conf = AppConf::load(&app).unwrap();\n    conf.amend(serde_json::json!({\"stay_on_top\": pin}))\n        .unwrap()\n        .save(&app)\n        .unwrap();\n\n    app.get_window(\"core\")\n        .unwrap()\n        .set_always_on_top(pin)\n        .unwrap();\n}\n\n#[command]\npub fn ask_sync(app: AppHandle, message: String) {\n    app.get_window(\"core\")\n        .unwrap()\n        .get_webview(\"main\")\n        .unwrap()\n        .eval(&format!(\"ChatAsk.sync({})\", message))\n        .unwrap();\n}\n\n#[command]\npub fn ask_send(app: AppHandle) {\n    let win = app.get_window(\"core\").unwrap();\n\n    win.get_webview(\"main\")\n        .unwrap()\n        .eval(\n            r#\"\n        ChatAsk.submit();\n        setTimeout(() => {\n            __TAURI__.webview.Webview.getByLabel('ask')?.setFocus();\n        }, 500);\n        \"#,\n        )\n        .unwrap();\n}\n\n#[command]\npub fn set_theme(app: AppHandle, theme: String) {\n    let conf = AppConf::load(&app).unwrap();\n    conf.amend(serde_json::json!({\"theme\": theme}))\n        .unwrap()\n        .save(&app)\n        .unwrap();\n\n    app.restart();\n}\n\n#[command]\npub fn get_app_conf(app: AppHandle) -> AppConf {\n    AppConf::load(&app).unwrap()\n}\n\n#[command]\npub fn set_view_ask(app: AppHandle, enabled: bool) {\n    let conf = AppConf::load(&app).unwrap();\n    conf.amend(serde_json::json!({\"ask_mode\": enabled}))\n        .unwrap()\n        .save(&app)\n        .unwrap();\n\n    let core_window = app.get_window(\"core\").unwrap();\n    let ask_mode_height = if enabled { ASK_HEIGHT } else { 0.0 };\n    let scale_factor = core_window.scale_factor().unwrap();\n    let titlebar_height = (scale_factor * TITLEBAR_HEIGHT).round() as u32;\n    let win_size = core_window.inner_size().unwrap();\n    let ask_height = (scale_factor * ask_mode_height).round() as u32;\n\n    let main_view = core_window.get_webview(\"main\").unwrap();\n    let titlebar_view = core_window.get_webview(\"titlebar\").unwrap();\n    let ask_view = core_window.get_webview(\"ask\").unwrap();\n\n    if enabled {\n        ask_view.set_focus().unwrap();\n    } else {\n        main_view.set_focus().unwrap();\n    }\n\n    let set_view_properties =\n        |view: &tauri::Webview, position: LogicalPosition<f64>, size: PhysicalSize<u32>| {\n            if let Err(e) = view.set_position(position) {\n                eprintln!(\"[cmd:view:position] Failed to set view position: {}\", e);\n            }\n            if let Err(e) = view.set_size(size) {\n                eprintln!(\"[cmd:view:size] Failed to set view size: {}\", e);\n            }\n        };\n\n    #[cfg(target_os = \"macos\")]\n    {\n        set_view_properties(\n            &main_view,\n            LogicalPosition::new(0.0, TITLEBAR_HEIGHT),\n            PhysicalSize::new(\n                win_size.width,\n                win_size.height - (titlebar_height + ask_height),\n            ),\n        );\n        set_view_properties(\n            &titlebar_view,\n            LogicalPosition::new(0.0, 0.0),\n            PhysicalSize::new(win_size.width, titlebar_height),\n        );\n        set_view_properties(\n            &ask_view,\n            LogicalPosition::new(\n                0.0,\n                (win_size.height as f64 / scale_factor) - ask_mode_height,\n            ),\n            PhysicalSize::new(win_size.width, ask_height),\n        );\n    }\n\n    #[cfg(not(target_os = \"macos\"))]\n    {\n        set_view_properties(\n            &main_view,\n            LogicalPosition::new(0.0, 0.0),\n            PhysicalSize::new(\n                win_size.width,\n                win_size.height - (ask_height + titlebar_height),\n            ),\n        );\n        set_view_properties(\n            &titlebar_view,\n            LogicalPosition::new(\n                0.0,\n                (win_size.height as f64 / scale_factor) - TITLEBAR_HEIGHT,\n            ),\n            PhysicalSize::new(win_size.width, titlebar_height),\n        );\n        set_view_properties(\n            &ask_view,\n            LogicalPosition::new(\n                0.0,\n                (win_size.height as f64 / scale_factor) - ask_mode_height - TITLEBAR_HEIGHT,\n            ),\n            PhysicalSize::new(win_size.width, ask_height),\n        );\n    }\n}\n"
  },
  {
    "path": "src-tauri/src/core/conf.rs",
    "content": "use log::error;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\nuse std::{\n    collections::BTreeMap,\n    fs::{self, File},\n    io::{Read, Write},\n    path::PathBuf,\n};\nuse tauri::{AppHandle, Manager, Theme};\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct AppConf {\n    pub theme: String,\n    pub stay_on_top: bool,\n    pub ask_mode: bool,\n    pub mac_titlebar_hidden: bool,\n}\n\nimpl AppConf {\n    pub fn new() -> Self {\n        Self {\n            theme: \"system\".to_string(),\n            stay_on_top: false,\n            ask_mode: false,\n            #[cfg(target_os = \"macos\")]\n            mac_titlebar_hidden: true,\n            #[cfg(not(target_os = \"macos\"))]\n            mac_titlebar_hidden: false,\n        }\n    }\n\n    pub fn get_conf_path(app: &AppHandle) -> Result<PathBuf, Box<dyn std::error::Error>> {\n        let config_dir = app\n            .path()\n            .config_dir()?\n            .join(\"com.nofwl.chatgpt\")\n            .join(\"config.json\");\n        Ok(config_dir)\n    }\n\n    pub fn get_scripts_path(app: &AppHandle) -> Result<PathBuf, Box<dyn std::error::Error>> {\n        let scripts_dir = app\n            .path()\n            .config_dir()?\n            .join(\"com.nofwl.chatgpt\")\n            .join(\"scripts\");\n        Ok(scripts_dir)\n    }\n\n    pub fn load_script(app: &AppHandle, filename: &str) -> String {\n        let script_file = Self::get_scripts_path(app).unwrap().join(filename);\n        fs::read_to_string(script_file).unwrap_or_else(|_| \"\".to_string())\n    }\n\n    pub fn load(app: &AppHandle) -> Result<Self, Box<dyn std::error::Error>> {\n        let path = Self::get_conf_path(app)?;\n\n        if !path.exists() {\n            let config = Self::new();\n            config.save(app)?;\n            return Ok(config);\n        }\n\n        let mut file = File::open(path)?;\n        let mut contents = String::new();\n        file.read_to_string(&mut contents)?;\n        let config: Result<AppConf, _> = serde_json::from_str(&contents);\n\n        // Handle conditional fields and fallback to defaults if necessary\n        if let Err(e) = &config {\n            error!(\"[conf::load] {}\", e);\n            let mut default_config = Self::new();\n            default_config = default_config.amend(serde_json::from_str(&contents)?)?;\n            default_config.save(app)?;\n            return Ok(default_config);\n        }\n\n        Ok(config?)\n    }\n\n    pub fn save(&self, app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {\n        let path = Self::get_conf_path(app)?;\n\n        if let Some(dir) = path.parent() {\n            fs::create_dir_all(dir)?;\n        }\n\n        let mut file = File::create(path)?;\n        let contents = serde_json::to_string_pretty(self)?;\n        // dbg!(&contents);\n        file.write_all(contents.as_bytes())?;\n        Ok(())\n    }\n\n    pub fn amend(self, json: Value) -> Result<Self, serde_json::Error> {\n        let val = serde_json::to_value(self)?;\n        let mut config: BTreeMap<String, Value> = serde_json::from_value(val)?;\n        let new_json: BTreeMap<String, Value> = serde_json::from_value(json)?;\n\n        for (k, v) in new_json {\n            config.insert(k, v);\n        }\n\n        let config_str = serde_json::to_string_pretty(&config)?;\n        serde_json::from_str::<AppConf>(&config_str).map_err(|err| {\n            error!(\"[conf::amend] {}\", err);\n            err\n        })\n    }\n\n    pub fn get_theme(app: &AppHandle) -> Theme {\n        let theme = Self::load(app).unwrap().theme;\n        match theme.as_str() {\n            \"system\" => match dark_light::detect() {\n                dark_light::Mode::Dark => Theme::Dark,\n                dark_light::Mode::Light => Theme::Light,\n                dark_light::Mode::Default => Theme::Light,\n            },\n            \"dark\" => Theme::Dark,\n            _ => Theme::Light,\n        }\n    }\n}\n"
  },
  {
    "path": "src-tauri/src/core/constant.rs",
    "content": "pub static TITLEBAR_HEIGHT: f64 = 28.0;\npub static ASK_HEIGHT: f64 = 120.0;\n\npub static WINDOW_SETTINGS: &str = \"settings\";\n\npub static INIT_SCRIPT: &str = r#\"\nwindow.addEventListener('DOMContentLoaded', function() {\n    function handleUrlChange() {\n        const url = window.location.href;\n        if (url !== 'about:blank') {\n            console.log('URL changed:', url);\n            window.__TAURI__.webviewWindow.WebviewWindow.getByLabel('titlebar').emit('navigation:change', { url });\n        }\n    }\n\n    function handleLinkClick(event) {\n        const target = event.target;\n        if (target.tagName === 'A' && target.target && target.target !== '_blank') {\n            target.target = '_blank';\n        }\n    }\n\n    document.addEventListener('click', handleLinkClick, true);\n    window.addEventListener('popstate', handleUrlChange);\n    window.addEventListener('pushState', handleUrlChange);\n    window.addEventListener('replaceState', handleUrlChange);\n\n    const originalPushState = history.pushState;\n    const originalReplaceState = history.replaceState;\n\n    history.pushState = function() {\n        originalPushState.apply(this, arguments);\n        console.log('pushState called');\n        handleUrlChange();\n    };\n\n    history.replaceState = function() {\n        originalReplaceState.apply(this, arguments);\n        console.log('replaceState called');\n        handleUrlChange();\n    };\n\n    handleUrlChange();\n});\n\"#;\n"
  },
  {
    "path": "src-tauri/src/core/mod.rs",
    "content": "pub mod cmd;\npub mod conf;\npub mod constant;\npub mod setup;\npub mod template;\npub mod window;\n"
  },
  {
    "path": "src-tauri/src/core/setup.rs",
    "content": "use std::{\n    path::PathBuf,\n    sync::{Arc, Mutex},\n};\nuse tauri::{\n    webview::DownloadEvent, App, LogicalPosition, Manager, PhysicalSize, WebviewBuilder,\n    WebviewUrl, WindowBuilder, WindowEvent,\n};\nuse tauri_plugin_shell::ShellExt;\n\n#[cfg(target_os = \"macos\")]\nuse tauri::TitleBarStyle;\n\nuse crate::core::{\n    conf::AppConf,\n    constant::{ASK_HEIGHT, INIT_SCRIPT, TITLEBAR_HEIGHT},\n    template,\n};\n\npub fn init(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {\n    let handle = app.handle();\n\n    let conf = &AppConf::load(handle)?;\n    let ask_mode_height = if conf.ask_mode { ASK_HEIGHT } else { 0.0 };\n\n    template::Template::new(AppConf::get_scripts_path(handle)?);\n\n    tauri::async_runtime::spawn({\n        let handle = handle.clone();\n        async move {\n            let mut core_window = WindowBuilder::new(&handle, \"core\").title(\"ChatGPT\");\n\n            #[cfg(target_os = \"macos\")]\n            {\n                core_window = core_window\n                    .title_bar_style(TitleBarStyle::Overlay)\n                    .hidden_title(true);\n            }\n\n            core_window = core_window\n                .resizable(true)\n                .inner_size(800.0, 600.0)\n                .min_inner_size(300.0, 200.0)\n                .theme(Some(AppConf::get_theme(&handle)));\n\n            let core_window = core_window\n                .build()\n                .expect(\"[core:window] Failed to build window\");\n\n            let win_size = core_window\n                .inner_size()\n                .expect(\"[core:window] Failed to get window size\");\n            // Wrap the window in Arc<Mutex<_>> to manage ownership across threads\n            let window = Arc::new(Mutex::new(core_window));\n\n            let main_view =\n                WebviewBuilder::new(\"main\", WebviewUrl::App(\"https://chatgpt.com\".into()))\n                    .auto_resize()\n                    .on_download({\n                        let app_handle = handle.clone();\n                        let download_path = Arc::new(Mutex::new(PathBuf::new()));\n                        move |_, event| {\n                            match event {\n                                DownloadEvent::Requested { destination, .. } => {\n                                    let download_dir = app_handle\n                                        .path()\n                                        .download_dir()\n                                        .expect(\"[view:download] Failed to get download directory\");\n                                    let mut locked_path = download_path\n                                        .lock()\n                                        .expect(\"[view:download] Failed to lock download path\");\n                                    *locked_path = download_dir.join(&destination);\n                                    *destination = locked_path.clone();\n                                }\n                                DownloadEvent::Finished { success, .. } => {\n                                    let final_path = download_path\n                                        .lock()\n                                        .expect(\"[view:download] Failed to lock download path\")\n                                        .clone();\n\n                                    if success {\n                                        app_handle\n                                            .shell()\n                                            .open(final_path.to_string_lossy(), None)\n                                            .expect(\"[view:download] Failed to open file\");\n                                    }\n                                }\n                                _ => (),\n                            }\n                            true\n                        }\n                    })\n                    .initialization_script(&AppConf::load_script(&handle, \"ask.js\"))\n                    .initialization_script(INIT_SCRIPT);\n\n            let titlebar_view = WebviewBuilder::new(\n                \"titlebar\",\n                WebviewUrl::App(\"index.html\".into()),\n            )\n            .auto_resize();\n\n            let ask_view =\n                WebviewBuilder::new(\"ask\", WebviewUrl::App(\"index.html\".into()))\n                    .auto_resize();\n\n            let win = window.lock().unwrap();\n            let scale_factor = win.scale_factor().unwrap();\n            let titlebar_height = (scale_factor * TITLEBAR_HEIGHT).round() as u32;\n            let ask_height = (scale_factor * ask_mode_height).round() as u32;\n\n            #[cfg(target_os = \"macos\")]\n            {\n                let main_area_height = win_size.height - titlebar_height;\n\n                win.add_child(\n                    titlebar_view,\n                    LogicalPosition::new(0, 0),\n                    PhysicalSize::new(win_size.width, titlebar_height),\n                )\n                .unwrap();\n                win.add_child(\n                    ask_view,\n                    LogicalPosition::new(\n                        0.0,\n                        (win_size.height as f64 / scale_factor) - ask_mode_height,\n                    ),\n                    PhysicalSize::new(win_size.width, ask_height),\n                )\n                .unwrap();\n                win.add_child(\n                    main_view,\n                    LogicalPosition::new(0.0, TITLEBAR_HEIGHT),\n                    PhysicalSize::new(win_size.width, main_area_height - ask_height),\n                )\n                .unwrap();\n            }\n\n            #[cfg(not(target_os = \"macos\"))]\n            {\n                win.add_child(\n                    ask_view,\n                    LogicalPosition::new(\n                        0.0,\n                        (win_size.height as f64 / scale_factor) - ask_mode_height,\n                    ),\n                    PhysicalSize::new(win_size.width, ask_height),\n                )\n                .unwrap();\n                win.add_child(\n                    titlebar_view,\n                    LogicalPosition::new(\n                        0.0,\n                        (win_size.height as f64 / scale_factor) - ask_mode_height - TITLEBAR_HEIGHT,\n                    ),\n                    PhysicalSize::new(win_size.width, titlebar_height),\n                )\n                .unwrap();\n                win.add_child(\n                    main_view,\n                    LogicalPosition::new(0.0, 0.0),\n                    PhysicalSize::new(\n                        win_size.width,\n                        win_size.height - (ask_height + titlebar_height),\n                    ),\n                )\n                .unwrap();\n            }\n\n            let window_clone = Arc::clone(&window);\n            let set_view_properties =\n                |view: &tauri::Webview, position: LogicalPosition<f64>, size: PhysicalSize<u32>| {\n                    if let Err(e) = view.set_position(position) {\n                        eprintln!(\"[view:position] Failed to set view position: {}\", e);\n                    }\n                    if let Err(e) = view.set_size(size) {\n                        eprintln!(\"[view:size] Failed to set view size: {}\", e);\n                    }\n                };\n\n            win.on_window_event(move |event| {\n                let conf = &AppConf::load(&handle).unwrap();\n                let ask_mode_height = if conf.ask_mode { ASK_HEIGHT } else { 0.0 };\n                let ask_height = (scale_factor * ask_mode_height).round() as u32;\n\n                if let WindowEvent::Resized(size) = event {\n                    let win = window_clone.lock().unwrap();\n\n                    let main_view = win\n                        .get_webview(\"main\")\n                        .expect(\"[view:main] Failed to get webview window\");\n                    let titlebar_view = win\n                        .get_webview(\"titlebar\")\n                        .expect(\"[view:titlebar] Failed to get webview window\");\n                    let ask_view = win\n                        .get_webview(\"ask\")\n                        .expect(\"[view:ask] Failed to get webview window\");\n\n                    #[cfg(target_os = \"macos\")]\n                    {\n                        set_view_properties(\n                            &main_view,\n                            LogicalPosition::new(0.0, TITLEBAR_HEIGHT),\n                            PhysicalSize::new(\n                                size.width,\n                                size.height - (titlebar_height + ask_height),\n                            ),\n                        );\n                        set_view_properties(\n                            &titlebar_view,\n                            LogicalPosition::new(0.0, 0.0),\n                            PhysicalSize::new(size.width, titlebar_height),\n                        );\n                        set_view_properties(\n                            &ask_view,\n                            LogicalPosition::new(\n                                0.0,\n                                (size.height as f64 / scale_factor) - ask_mode_height,\n                            ),\n                            PhysicalSize::new(size.width, ask_height),\n                        );\n                    }\n\n                    #[cfg(not(target_os = \"macos\"))]\n                    {\n                        set_view_properties(\n                            &main_view,\n                            LogicalPosition::new(0.0, 0.0),\n                            PhysicalSize::new(\n                                size.width,\n                                size.height - (ask_height + titlebar_height),\n                            ),\n                        );\n                        set_view_properties(\n                            &titlebar_view,\n                            LogicalPosition::new(\n                                0.0,\n                                (size.height as f64 / scale_factor) - TITLEBAR_HEIGHT,\n                            ),\n                            PhysicalSize::new(size.width, titlebar_height),\n                        );\n                        set_view_properties(\n                            &ask_view,\n                            LogicalPosition::new(\n                                0.0,\n                                (size.height as f64 / scale_factor)\n                                    - ask_mode_height\n                                    - TITLEBAR_HEIGHT,\n                            ),\n                            PhysicalSize::new(size.width, ask_height),\n                        );\n                    }\n                }\n            });\n        }\n    });\n\n    Ok(())\n}\n"
  },
  {
    "path": "src-tauri/src/core/template.rs",
    "content": "use anyhow::{Context, Result};\nuse log::{error, info};\nuse regex::Regex;\nuse semver::Version;\nuse serde_json::json;\nuse std::{\n    fs::{self, File},\n    io::{Read, Write},\n    path::Path,\n};\n\npub static SCRIPT_ASK: &[u8] = include_bytes!(\"../../scripts/ask.js\");\n\n/// Struct representing the template with the script data.\n#[derive(Debug)]\npub struct Template {\n    pub ask: Vec<u8>,\n}\n\nimpl Template {\n    /// Creates a new Template instance, initializing it with the script data.\n    pub fn new<P: AsRef<Path>>(template_dir: P) -> Self {\n        let template_dir = template_dir.as_ref();\n        let mut template = Template::default();\n\n        let files = vec![(template_dir.join(\"ask.js\"), &mut template.ask)];\n\n        for (filename, _) in files {\n            match update_or_create_file(&filename, SCRIPT_ASK) {\n                Ok(updated) => {\n                    if updated {\n                        info!(\"Script updated or created: {}\", filename.display());\n                    } else {\n                        info!(\"Script is up-to-date: {}\", filename.display());\n                    }\n                }\n                Err(e) => {\n                    error!(\"Failed to process script, {}: {}\", filename.display(), e);\n                }\n            }\n        }\n\n        template\n    }\n}\n\nimpl Default for Template {\n    fn default() -> Template {\n        Template {\n            ask: Vec::from(SCRIPT_ASK),\n        }\n    }\n}\n\n/// Reads the version information from the given data.\nfn read_version_info(data: &[u8]) -> Result<serde_json::Value> {\n    let content = String::from_utf8_lossy(data);\n    let re_name = Regex::new(r\"@name\\s+(.*?)\\n\").context(\"Failed to compile name regex\")?;\n    let re_version =\n        Regex::new(r\"@version\\s+(.*?)\\n\").context(\"Failed to compile version regex\")?;\n    let re_url = Regex::new(r\"@url\\s+(.*?)\\n\").context(\"Failed to compile url regex\")?;\n\n    let name = re_name\n        .captures(&content)\n        .and_then(|cap| cap.get(1))\n        .map_or(String::new(), |m| m.as_str().trim().to_string());\n\n    let version = re_version\n        .captures(&content)\n        .and_then(|cap| cap.get(1))\n        .map_or(String::new(), |m| m.as_str().trim().to_string());\n\n    let url = re_url\n        .captures(&content)\n        .and_then(|cap| cap.get(1))\n        .map_or(String::new(), |m| m.as_str().trim().to_string());\n\n    let json_data = json!({\n        \"name\": name,\n        \"version\": version,\n        \"url\": url,\n    });\n\n    Ok(json_data)\n}\n\n/// Reads the contents of the given file.\nfn read_file_contents<P: AsRef<Path>>(filename: P) -> Result<Vec<u8>> {\n    let filename = filename.as_ref();\n    let mut file = File::open(filename)?;\n    let mut contents = Vec::new();\n    file.read_to_end(&mut contents)?;\n    Ok(contents)\n}\n\n/// Writes the given data to the specified file.\nfn write_file_contents<P: AsRef<Path>>(filename: P, data: &[u8]) -> Result<()> {\n    let filename = filename.as_ref();\n    let mut file = File::create(filename)?;\n    file.write_all(data)?;\n    Ok(())\n}\n\n/// Creates the necessary directories for the specified file path.\nfn create_dir<P: AsRef<Path>>(filename: P) -> Result<()> {\n    let filename = filename.as_ref();\n    if let Some(parent) = filename.parent() {\n        if !parent.exists() {\n            fs::create_dir_all(parent)?;\n        }\n    }\n    Ok(())\n}\n\n/// Updates the file if the new data has a newer version or if version info is missing,\n/// or creates the file if it doesn't exist.\nfn update_or_create_file<P: AsRef<Path>>(filename: P, new_data: &[u8]) -> Result<bool> {\n    let filename = filename.as_ref();\n\n    // Ensure directory exists\n    create_dir(filename)?;\n\n    let current_data = read_file_contents(filename);\n\n    match current_data {\n        Ok(current_data) => {\n            let new_info = read_version_info(new_data)?;\n            let current_info = read_version_info(&current_data);\n\n            match (\n                new_info.get(\"version\").and_then(|v| v.as_str()),\n                current_info,\n            ) {\n                (Some(new_version), Ok(current_info)) => {\n                    let current_version = current_info\n                        .get(\"version\")\n                        .and_then(|v| v.as_str())\n                        .unwrap_or(\"\");\n\n                    if current_version.is_empty()\n                        || Version::parse(new_version)? > Version::parse(current_version)?\n                    {\n                        write_file_contents(filename, new_data)?;\n                        info!(\"{} → {}\", current_version, new_version);\n                        Ok(true)\n                    } else {\n                        Ok(false)\n                    }\n                }\n                // If there is an error reading current version info, update the file\n                (Some(_), Err(_)) => {\n                    write_file_contents(filename, new_data)?;\n                    Ok(true)\n                }\n                (None, _) => {\n                    // If there is an error reading new version info, don't update the file\n                    Ok(false)\n                }\n            }\n        }\n        Err(_) => {\n            // If there is an error reading the current file, create a new file\n            write_file_contents(filename, new_data)?;\n            Ok(true)\n        }\n    }\n}\n"
  },
  {
    "path": "src-tauri/src/core/window.rs",
    "content": "use tauri::{command, AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};\n\nuse crate::core::constant::WINDOW_SETTINGS;\n\n#[command]\npub fn open_settings(app: AppHandle) {\n  match app.get_webview_window(WINDOW_SETTINGS) {\n    Some(window) => {\n      window.show().unwrap();\n    }\n    None => {\n      WebviewWindowBuilder::new(&app, WINDOW_SETTINGS, WebviewUrl::App(\"index.html\".into()))\n        .build()\n        .unwrap();\n    }\n  }\n}\n"
  },
  {
    "path": "src-tauri/src/main.rs",
    "content": "// Prevents additional console window on Windows in release, DO NOT REMOVE!!\n#![cfg_attr(not(debug_assertions), windows_subsystem = \"windows\")]\n\nmod core;\nuse core::{cmd, setup, window};\n\nfn main() {\n    tauri::Builder::default()\n        .plugin(tauri_plugin_os::init())\n        .plugin(tauri_plugin_shell::init())\n        .plugin(tauri_plugin_dialog::init())\n        .invoke_handler(tauri::generate_handler![\n            cmd::view_reload,\n            cmd::view_url,\n            cmd::view_go_forward,\n            cmd::view_go_back,\n            cmd::set_view_ask,\n            cmd::get_app_conf,\n            cmd::window_pin,\n            cmd::ask_sync,\n            cmd::ask_send,\n            cmd::set_theme,\n            window::open_settings,\n        ])\n        .setup(setup::init)\n        .run(tauri::generate_context!())\n        .expect(\"error while running lencx/ChatGPT application\");\n}\n"
  },
  {
    "path": "src-tauri/tauri.conf.json",
    "content": "{\n  \"productName\": \"ChatGPT\",\n  \"version\": \"../package.json\",\n  \"identifier\": \"com.nofwl.chatgpt\",\n  \"build\": {\n    \"beforeDevCommand\": \"pnpm dev\",\n    \"devUrl\": \"http://localhost:1420\",\n    \"beforeBuildCommand\": \"pnpm build\",\n    \"frontendDist\": \"../dist\"\n  },\n  \"app\": {\n    \"withGlobalTauri\": true,\n    \"windows\": [],\n    \"security\": {\n      \"csp\": null\n    }\n  },\n  \"bundle\": {\n    \"active\": true,\n    \"targets\": \"all\",\n    \"icon\": [\n      \"icons/32x32.png\",\n      \"icons/128x128.png\",\n      \"icons/128x128@2x.png\",\n      \"icons/icon.icns\",\n      \"icons/icon.ico\"\n    ]\n  }\n}\n"
  },
  {
    "path": "tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nexport default {\n  content: [\n    './index.html',\n    './src/**/*.{js,jsx,ts,tsx}',\n  ],\n  theme: {\n    screens: {\n      tablet: '480px',\n    },\n    extend: {\n      colors: {\n        'app-gray-1': '#171717',\n        'app-gray-2': '#212121',\n        'app-gray-3': '#2f2f2f;',\n        'app-active': '#10a37f',\n      }\n    },\n  },\n  plugins: [\n    require('@tailwindcss/typography'),\n    require('autoprefixer'),\n  ],\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true,\n\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"~/*\": [\"src/*\"],\n      \"~components/*\": [\"src/components/*\"],\n      \"~view/*\": [\"src/view/*\"],\n      \"~hooks/*\": [\"src/hooks/*\"],\n      \"~utils/*\": [\"src/utils/*\"],\n      \"~icons/*\": [\"src/icons/*\"],\n      \"~layout/*\": [\"src/layout/*\"]\n    }\n  },\n  \"include\": [\"src\"],\n  \"references\": [{ \"path\": \"./tsconfig.node.json\" }]\n}\n"
  },
  {
    "path": "tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "vite.config.ts",
    "content": "import { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport tsconfigPaths from 'vite-tsconfig-paths';\n\n// https://vitejs.dev/config/\nexport default defineConfig(async () => ({\n  plugins: [tsconfigPaths(), react()],\n\n  // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`\n  //\n  // 1. prevent vite from obscuring rust errors\n  clearScreen: false,\n  // 2. tauri expects a fixed port, fail if that port is not available\n  server: {\n    port: 1420,\n    strictPort: true,\n    watch: {\n      // 3. tell vite to ignore watching `src-tauri`\n      ignored: ['**/src-tauri/**'],\n    },\n  },\n}));\n"
  }
]