From 6c9f2bc3c4f7179074c68f7f36e331d40d02a3bd Mon Sep 17 00:00:00 2001 From: liuhuapiaoyuan <278780765@qq.com> Date: Sun, 27 Oct 2024 19:50:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0url=E9=98=85=E8=AF=BB?= =?UTF-8?q?=E8=83=BD=E5=8A=9B,=E9=80=9A=E8=BF=87=20jina.ai=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + backend/api/routes/chat.py | 35 ++++- backend/main.py | 2 +- backend/utils.py | 12 +- frontend/.env.template | 2 + frontend/package.json | 1 + frontend/pnpm-lock.yaml | 36 +++++ frontend/src/App.tsx | 2 + frontend/src/components/menu.tsx | 143 +++++++++++++----- frontend/src/components/ui/toast.tsx | 127 ++++++++++++++++ frontend/src/components/ui/toaster.tsx | 33 +++++ frontend/src/hooks/use-toast.ts | 194 +++++++++++++++++++++++++ frontend/src/index.css | 7 + frontend/src/lib/constant.ts | 5 +- frontend/src/vite-env.d.ts | 2 +- frontend/vite.config.ts | 2 + 16 files changed, 556 insertions(+), 48 deletions(-) create mode 100644 frontend/.env.template create mode 100644 frontend/src/components/ui/toast.tsx create mode 100644 frontend/src/components/ui/toaster.tsx create mode 100644 frontend/src/hooks/use-toast.ts diff --git a/.gitignore b/.gitignore index a6dd346..5997da4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules/ /playwright-report/ /blob-report/ /playwright/.cache/ +.env \ No newline at end of file diff --git a/backend/api/routes/chat.py b/backend/api/routes/chat.py index 2a2dab5..81a9135 100644 --- a/backend/api/routes/chat.py +++ b/backend/api/routes/chat.py @@ -4,7 +4,7 @@ import json from typing import Dict, Optional from constants import SPEEKERS -from utils import combine_audio, generate_dialogue, generate_podcast_info, generate_podcast_summary, get_pdf_text +from utils import combine_audio, generate_dialogue, generate_podcast_info, generate_podcast_summary, get_link_text, get_pdf_text router = APIRouter() @@ -12,12 +12,19 @@ async def generate_transcript( pdfFile: Optional[UploadFile] = File(None), textInput: str = Form(...), + mode: str = Form(...), + url: Optional[str] = Form(None), tone: str = Form(...), duration: str = Form(...), language: str = Form(...), ): - pdfContent = await get_pdf_text(pdfFile) + pdfContent ="" + if mode=='pdf': + pdfContent = await get_pdf_text(pdfFile) + else: + linkData = get_link_text(url) + pdfContent = linkData['text'] new_text = pdfContent return StreamingResponse(generate_dialogue(new_text,textInput, tone, duration, language), media_type="application/json") @@ -31,6 +38,11 @@ def test(): def speeker(): return JSONResponse(content=SPEEKERS) +@router.get("/jina") +def jina(): + result = get_link_text("https://ui.shadcn.com/docs/components/select") + return JSONResponse(content=result) + @router.post("/summarize") async def get_summary( @@ -38,9 +50,16 @@ async def get_summary( tone: str = Form(...), duration: str = Form(...), language: str = Form(...), + mode: str = Form(...), + url: Optional[str] = Form(None), pdfFile: Optional[UploadFile] = File(None) ): - pdfContent = await get_pdf_text(pdfFile) + pdfContent ="" + if mode=='pdf': + pdfContent = await get_pdf_text(pdfFile) + else: + linkData = get_link_text(url) + pdfContent = linkData['text'] new_text = pdfContent return StreamingResponse( generate_podcast_summary( @@ -59,9 +78,17 @@ async def get_pod_info( tone: str = Form(...), duration: str = Form(...), language: str = Form(...), + mode: str = Form(...), + url: Optional[str] = Form(None), pdfFile: Optional[UploadFile] = File(None) ): - pdfContent = await get_pdf_text(pdfFile) + pdfContent ="" + if mode=='pdf': + pdfContent = await get_pdf_text(pdfFile) + else: + linkData = get_link_text(url) + pdfContent = linkData['text'] + new_text = pdfContent[:100] return StreamingResponse(generate_podcast_info(new_text, textInput, tone, duration, language), media_type="application/json") diff --git a/backend/main.py b/backend/main.py index 2395825..b9641f3 100644 --- a/backend/main.py +++ b/backend/main.py @@ -16,7 +16,7 @@ # 添加CORS中间件 app.add_middleware( CORSMiddleware, - allow_origins=["https://ai.podcastlm.fun/"], + allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], diff --git a/backend/utils.py b/backend/utils.py index f50fafc..c0b26fc 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -5,10 +5,10 @@ import re import time import hashlib - from typing import Any, Dict, Generator import uuid from openai import OpenAI +import requests from fishaudio import fishaudio_tts from prompts import LANGUAGE_MODIFIER, LENGTH_MODIFIERS, PODCAST_INFO_PROMPT, QUESTION_MODIFIER, SUMMARY_INFO_PROMPT, SYSTEM_PROMPT, TONE_MODIFIER import json @@ -254,6 +254,16 @@ def clear_pdf_cache(): global pdf_cache pdf_cache.clear() +def get_link_text(url: str): + """ 通过jina.ai 抓取url内容 """ + url = f"https://r.jina.ai/{url}" + headers = {} + headers['Authorization'] = 'Bearer jina_c1759c7f49e14ced990ac7776800dc44ShJNTXBCizzwjE7IMFYJ6LD960cG' + headers['Accept'] = 'application/json' + headers['X-Return-Format'] = 'text' + response = requests.get(url, headers=headers) + return response.json()['data'] + async def get_pdf_text(pdf_file: UploadFile): text = "" print(pdf_file) diff --git a/frontend/.env.template b/frontend/.env.template new file mode 100644 index 0000000..1a144f6 --- /dev/null +++ b/frontend/.env.template @@ -0,0 +1,2 @@ +BASE_URL= +HOST_URL= \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 3d60828..1679d61 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.1", + "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0", "axios": "^1.7.7", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 2dc25e9..4801a01 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: '@radix-ui/react-tabs': specifier: ^1.1.1 version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toast': + specifier: ^1.2.2 + version: 1.2.2(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-toggle': specifier: ^1.1.0 version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -759,6 +762,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-toast@1.2.2': + resolution: {integrity: sha512-Z6pqSzmAP/bFJoqMAston4eSNa+ud44NSZTiZUmUen+IOZ5nBY8kzuU5WDBVyFXPtcW6yUalOHsxM/BP6Sv8ww==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-toggle-group@1.1.0': resolution: {integrity: sha512-PpTJV68dZU2oqqgq75Uzto5o/XfOVgkrJ9rulVmfTKxWp3HfUjHE6CP/WLRR4AzPX9HWxw7vFow2me85Yu+Naw==} peerDependencies: @@ -3029,6 +3045,26 @@ snapshots: '@types/react': 18.3.11 '@types/react-dom': 18.3.0 + '@radix-ui/react-toast@1.2.2(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.0 + '@radix-ui/react-toggle-group@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ecfb0c3..dd8cb7d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,6 +5,7 @@ import { useJsonData } from "./hooks/useJsonData"; import { useStreamText } from './hooks/useStreamText'; import { BASE_URL } from "./lib/constant"; import MobileMenu from "./components/mobile-menu"; +import { Toaster } from "@/components/ui/toaster" function App() { const [isGenerating, setIsGenerating] = useState(false); @@ -37,6 +38,7 @@ function App() { } return (
+
void, isGenerating: boolean }) { +export default function Menu({ handleGenerate, className, isGenerating }: { className?: string, handleGenerate: (formData: FormData) => void, isGenerating: boolean }) { + const { toast } = useToast() + const [pdfFile, setPdfFile] = useState(null); const [textInput, setTextInput] = useState(''); const [tone, setTone] = useState('neutral'); @@ -19,6 +24,9 @@ export default function Menu({ handleGenerate,className, isGenerating }: { class const [guestVoice, setGuestVoice] = useState('zh-CN-YunzeNeural'); const [provider, setProvider] = useState('azure'); const [fileError, setFileError] = useState(null); + const [url, setUrl] = useState(''); + const [mode,setMode] = useState<'pdf'|'url'>('pdf'); + const speekerReq = useSpeeker() @@ -49,14 +57,32 @@ export default function Menu({ handleGenerate,className, isGenerating }: { class } }; - const handleSubmit = () => { - if (!pdfFile) { + + const handleSubmit = () => { + if (mode=='pdf' && !pdfFile) { setFileError('Please upload a PDF file.'); + toast({ + title: "填写错误", + variant: "destructive" , + description: "Please upload a PDF file.", + }) return; } + setFileError(''); + if (mode=='url' ) { + const urlInValid = !url || !url.startsWith("http"); + if(urlInValid){ + toast({ + title: "填写错误", + variant: "destructive" , + description: "请检查url是否正确", + }) + return; + } + } const formData = new FormData(); - formData.append('pdfFile', pdfFile); + mode=='pdf' && pdfFile && formData.append('pdfFile', pdfFile); formData.append('textInput', textInput); formData.append('tone', tone); formData.append('duration', duration); @@ -64,43 +90,84 @@ export default function Menu({ handleGenerate,className, isGenerating }: { class formData.append('hostVoice', hostVoice); formData.append('guestVoice', guestVoice); formData.append('provider', provider); + mode=='url' && url &&formData.append('url', url); + formData.append('mode', mode); handleGenerate(formData); }; return ( -
+
-
-

上传 PDF *

-
- + setMode(e as 'pdf')} className="w-full h-full flex flex-col"> +
+ + + PDF访谈 + + + 网页访谈 + + + +

上传 PDF *

+
+ + +
+ {fileError &&

{fileError}

} +
+ 试一试: + +
+
+ +

URL 抓取 *

+ setUrl(e.target.value)} + placeholder="请输入 URL" /> - -
- {fileError &&

{fileError}

} -
- 试一试: - + {fileError &&

{fileError}

} +
+ Demo: + +
+
+
+
@@ -159,10 +226,10 @@ export default function Menu({ handleGenerate,className, isGenerating }: { class

Provider

- { setProvider(newProvider) const voices = speekerReq.data?.[newProvider]; - if(voices){ + if (voices) { setHostVoice(voices[0].id) setGuestVoice(voices[1].id) } @@ -172,7 +239,7 @@ export default function Menu({ handleGenerate,className, isGenerating }: { class { - Object.keys(speekerReq.data ?? {}).map(item => {item}) diff --git a/frontend/src/components/ui/toast.tsx b/frontend/src/components/ui/toast.tsx new file mode 100644 index 0000000..f79747a --- /dev/null +++ b/frontend/src/components/ui/toast.tsx @@ -0,0 +1,127 @@ +import * as React from "react" +import { Cross2Icon } from "@radix-ui/react-icons" +import * as ToastPrimitives from "@radix-ui/react-toast" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const ToastProvider = ToastPrimitives.Provider + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastViewport.displayName = ToastPrimitives.Viewport.displayName + +const toastVariants = cva( + "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", + { + variants: { + variant: { + default: "border bg-background text-foreground", + destructive: + "destructive group border-destructive bg-destructive text-destructive-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => { + return ( + + ) +}) +Toast.displayName = ToastPrimitives.Root.displayName + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastAction.displayName = ToastPrimitives.Action.displayName + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ToastClose.displayName = ToastPrimitives.Close.displayName + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastTitle.displayName = ToastPrimitives.Title.displayName + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastDescription.displayName = ToastPrimitives.Description.displayName + +type ToastProps = React.ComponentPropsWithoutRef + +type ToastActionElement = React.ReactElement + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, +} diff --git a/frontend/src/components/ui/toaster.tsx b/frontend/src/components/ui/toaster.tsx new file mode 100644 index 0000000..6c67edf --- /dev/null +++ b/frontend/src/components/ui/toaster.tsx @@ -0,0 +1,33 @@ +import { useToast } from "@/hooks/use-toast" +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "@/components/ui/toast" + +export function Toaster() { + const { toasts } = useToast() + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
+ {title && {title}} + {description && ( + {description} + )} +
+ {action} + +
+ ) + })} + +
+ ) +} diff --git a/frontend/src/hooks/use-toast.ts b/frontend/src/hooks/use-toast.ts new file mode 100644 index 0000000..02e111d --- /dev/null +++ b/frontend/src/hooks/use-toast.ts @@ -0,0 +1,194 @@ +"use client" + +// Inspired by react-hot-toast library +import * as React from "react" + +import type { + ToastActionElement, + ToastProps, +} from "@/components/ui/toast" + +const TOAST_LIMIT = 1 +const TOAST_REMOVE_DELAY = 1000000 + +type ToasterToast = ToastProps & { + id: string + title?: React.ReactNode + description?: React.ReactNode + action?: ToastActionElement +} + +const actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const + +let count = 0 + +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER + return count.toString() +} + +type ActionType = typeof actionTypes + +type Action = + | { + type: ActionType["ADD_TOAST"] + toast: ToasterToast + } + | { + type: ActionType["UPDATE_TOAST"] + toast: Partial + } + | { + type: ActionType["DISMISS_TOAST"] + toastId?: ToasterToast["id"] + } + | { + type: ActionType["REMOVE_TOAST"] + toastId?: ToasterToast["id"] + } + +interface State { + toasts: ToasterToast[] +} + +const toastTimeouts = new Map>() + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId) + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }) + }, TOAST_REMOVE_DELAY) + + toastTimeouts.set(toastId, timeout) +} + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + } + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t + ), + } + + case "DISMISS_TOAST": { + const { toastId } = action + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId) + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id) + }) + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t + ), + } + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + } + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + } + } +} + +const listeners: Array<(state: State) => void> = [] + +let memoryState: State = { toasts: [] } + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action) + listeners.forEach((listener) => { + listener(memoryState) + }) +} + +type Toast = Omit + +function toast({ ...props }: Toast) { + const id = genId() + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }) + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss() + }, + }, + }) + + return { + id: id, + dismiss, + update, + } +} + +function useToast() { + const [state, setState] = React.useState(memoryState) + + React.useEffect(() => { + listeners.push(setState) + return () => { + const index = listeners.indexOf(setState) + if (index > -1) { + listeners.splice(index, 1) + } + } + }, [state]) + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + } +} + +export { useToast, toast } diff --git a/frontend/src/index.css b/frontend/src/index.css index eed21a3..c8d1de7 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -13,12 +13,19 @@ :root { --background: #ffffff; --foreground: #171717; + --radius: 10px; + --input: 220deg 13.04% 90.98%; + --destructive: 0 72.22% 50.59%; + --destructive-foreground: 0 0% 98%; } @media (prefers-color-scheme: dark) { :root { --background: #0a0a0a; --foreground: #ededed; + --input: 220deg 13.04% 90.98%; + --destructive: 0 72.22% 50.59%; + --destructive-foreground: 0 0% 98%; } } diff --git a/frontend/src/lib/constant.ts b/frontend/src/lib/constant.ts index 9393b19..0291314 100644 --- a/frontend/src/lib/constant.ts +++ b/frontend/src/lib/constant.ts @@ -1,3 +1,2 @@ -export const BASE_URL = "https://zhang-xxx-podcastlm-backend.hf.space/api/v1/chat" -export const HOST_URL = "https://zhang-xxx-podcastlm-backend.hf.space" - +export const BASE_URL = import.meta.env.VITE_BASE_URL +export const HOST_URL = import.meta.env.VITE_HOST_URL \ No newline at end of file diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts index 11f02fe..151aa68 100644 --- a/frontend/src/vite-env.d.ts +++ b/frontend/src/vite-env.d.ts @@ -1 +1 @@ -/// +/// \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 0201731..a47a492 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,8 +1,10 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import { resolve } from 'path'; // https://vitejs.dev/config/ export default defineConfig({ + envDir: resolve(__dirname, '.'), plugins: [react()], resolve: { alias: {