Spaces:
Running
Running
[NOTICKET] Major update, re-stylign and upgrade using maintiva demo setup
Browse files- .env.example +3 -1
- .gitattributes +1 -0
- .gitignore +8 -1
- ATTRIBUTIONS.md +0 -3
- Dockerfile +5 -0
- index.html +2 -2
- package-lock.json +1799 -20
- public/maintiva-logo.jpg +0 -0
- public/sounds/01_Baik_Saya_sedang_memproses_pertanyaanmu.wav +3 -0
- public/sounds/02_Oke_mohon_ditunggu_saya_sedang_siapkan_P.wav +3 -0
- public/sounds/03_Sip_saya_terima_Sedang_saya_proses_Pesan.wav +3 -0
- server.js +13 -1
- src/app/App.tsx +7 -1
- src/app/components/KnowledgeManagement.tsx +525 -113
- src/app/components/Login.tsx +157 -38
- src/app/components/Main.tsx +364 -552
- src/app/components/chat/ChatInput.tsx +107 -0
- src/app/components/chat/ChatLayout.tsx +17 -0
- src/app/components/chat/ChatWindow.tsx +89 -0
- src/app/components/chat/FeedbackWidget.tsx +148 -0
- src/app/components/chat/MessageBubble.tsx +92 -0
- src/app/components/chat/Sidebar.tsx +282 -0
- src/app/components/chat/TypingIndicator.tsx +70 -0
- src/app/components/chat/VoiceMicButton.tsx +86 -0
- src/app/components/chat/VoiceStatusBar.tsx +73 -0
- src/app/components/chat/renderers/MarkdownRenderer.tsx +134 -0
- src/app/components/chat/types.ts +27 -0
- src/assets/logo.png +0 -0
- src/assets/maintiva-logo.jpg +0 -0
- src/audio/AudioPlayer.ts +158 -0
- src/audio/AudioRecorder.ts +70 -0
- src/audio/RecorderWorkletProcessor.js +12 -0
- src/hooks/useVoiceSession.ts +195 -0
- src/services/api.ts +90 -1
- src/services/voiceApi.ts +94 -0
- src/styles/theme.css +35 -0
- src/vite-env.d.ts +21 -0
- tsconfig.json +24 -0
.env.example
CHANGED
|
@@ -1 +1,3 @@
|
|
| 1 |
-
VITE_API_BASE_URL=
|
|
|
|
|
|
|
|
|
| 1 |
+
VITE_API_BASE_URL=
|
| 2 |
+
VITE_API_BASE_VOICE_URL=
|
| 3 |
+
VITE_API_BASE_VOICE_WS_URL=
|
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
public/sounds/* filter=lfs diff=lfs merge=lfs -text
|
.gitignore
CHANGED
|
@@ -36,4 +36,11 @@ Thumbs.db
|
|
| 36 |
*.local
|
| 37 |
vite.config.ts.timestamp-*
|
| 38 |
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
*.local
|
| 37 |
vite.config.ts.timestamp-*
|
| 38 |
|
| 39 |
+
API_CONTRACT_CHATBOT.md
|
| 40 |
+
API_CONTRACT_VOICE.md
|
| 41 |
+
STYLE.md
|
| 42 |
+
HIGHLIGHT_VOICE.md
|
| 43 |
+
HIGHLIGHT_STT_TTS.md
|
| 44 |
+
|
| 45 |
+
# Database logos (served via CDN)
|
| 46 |
+
public/databases/
|
ATTRIBUTIONS.md
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
This Figma Make file includes components from [shadcn/ui](https://ui.shadcn.com/) used under [MIT license](https://github.com/shadcn-ui/ui/blob/main/LICENSE.md).
|
| 2 |
-
|
| 3 |
-
This Figma Make file includes photos from [Unsplash](https://unsplash.com) used under [license](https://unsplash.com/license).
|
|
|
|
|
|
|
|
|
|
|
|
Dockerfile
CHANGED
|
@@ -8,6 +8,11 @@ WORKDIR /app
|
|
| 8 |
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml* pnpm-lock.json* ./
|
| 9 |
RUN pnpm install --no-frozen-lockfile
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
COPY . .
|
| 12 |
RUN pnpm build
|
| 13 |
|
|
|
|
| 8 |
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml* pnpm-lock.json* ./
|
| 9 |
RUN pnpm install --no-frozen-lockfile
|
| 10 |
|
| 11 |
+
ARG VITE_API_BASE_VOICE_URL
|
| 12 |
+
ARG VITE_API_BASE_VOICE_WS_URL
|
| 13 |
+
ENV VITE_API_BASE_VOICE_URL=$VITE_API_BASE_VOICE_URL
|
| 14 |
+
ENV VITE_API_BASE_VOICE_WS_URL=$VITE_API_BASE_VOICE_WS_URL
|
| 15 |
+
|
| 16 |
COPY . .
|
| 17 |
RUN pnpm build
|
| 18 |
|
index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
| 4 |
<head>
|
| 5 |
<meta charset="UTF-8" />
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
-
<link rel="icon" type="image/
|
| 8 |
-
<title>
|
| 9 |
</head>
|
| 10 |
|
| 11 |
<body>
|
|
|
|
| 4 |
<head>
|
| 5 |
<meta charset="UTF-8" />
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<link rel="icon" type="image/jpeg" href="/maintiva-logo.jpg" />
|
| 8 |
+
<title>Maintiva Agent</title>
|
| 9 |
</head>
|
| 10 |
|
| 11 |
<body>
|
package-lock.json
CHANGED
|
@@ -46,6 +46,7 @@
|
|
| 46 |
"date-fns": "3.6.0",
|
| 47 |
"embla-carousel-react": "8.6.0",
|
| 48 |
"input-otp": "1.4.2",
|
|
|
|
| 49 |
"lucide-react": "0.487.0",
|
| 50 |
"motion": "12.23.24",
|
| 51 |
"next-themes": "0.4.6",
|
|
@@ -53,12 +54,16 @@
|
|
| 53 |
"react-dnd": "16.0.1",
|
| 54 |
"react-dnd-html5-backend": "16.0.1",
|
| 55 |
"react-hook-form": "7.55.0",
|
|
|
|
| 56 |
"react-popper": "2.3.0",
|
| 57 |
"react-resizable-panels": "2.1.7",
|
| 58 |
"react-responsive-masonry": "2.7.1",
|
| 59 |
"react-router": "7.13.0",
|
| 60 |
"react-slick": "0.31.0",
|
| 61 |
"recharts": "2.15.2",
|
|
|
|
|
|
|
|
|
|
| 62 |
"sonner": "2.0.3",
|
| 63 |
"tailwind-merge": "3.2.0",
|
| 64 |
"tw-animate-css": "1.3.8",
|
|
@@ -3341,11 +3346,58 @@
|
|
| 3341 |
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
| 3342 |
"license": "MIT"
|
| 3343 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3344 |
"node_modules/@types/estree": {
|
| 3345 |
"version": "1.0.8",
|
| 3346 |
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
| 3347 |
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
| 3348 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3349 |
"license": "MIT"
|
| 3350 |
},
|
| 3351 |
"node_modules/@types/parse-json": {
|
|
@@ -3379,6 +3431,18 @@
|
|
| 3379 |
"@types/react": "*"
|
| 3380 |
}
|
| 3381 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3382 |
"node_modules/@vitejs/plugin-react": {
|
| 3383 |
"version": "4.7.0",
|
| 3384 |
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
|
@@ -3427,6 +3491,16 @@
|
|
| 3427 |
"npm": ">=6"
|
| 3428 |
}
|
| 3429 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3430 |
"node_modules/baseline-browser-mapping": {
|
| 3431 |
"version": "2.10.16",
|
| 3432 |
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz",
|
|
@@ -3514,6 +3588,56 @@
|
|
| 3514 |
"url": "https://www.paypal.me/kirilvatev"
|
| 3515 |
}
|
| 3516 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3517 |
"node_modules/chownr": {
|
| 3518 |
"version": "3.0.0",
|
| 3519 |
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
|
@@ -3567,6 +3691,25 @@
|
|
| 3567 |
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
| 3568 |
}
|
| 3569 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3570 |
"node_modules/convert-source-map": {
|
| 3571 |
"version": "1.9.0",
|
| 3572 |
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
|
|
@@ -3771,6 +3914,28 @@
|
|
| 3771 |
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
| 3772 |
"license": "MIT"
|
| 3773 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3774 |
"node_modules/detect-libc": {
|
| 3775 |
"version": "2.1.2",
|
| 3776 |
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
|
@@ -3787,6 +3952,19 @@
|
|
| 3787 |
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
| 3788 |
"license": "MIT"
|
| 3789 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3790 |
"node_modules/dnd-core": {
|
| 3791 |
"version": "16.0.1",
|
| 3792 |
"resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz",
|
|
@@ -3857,6 +4035,18 @@
|
|
| 3857 |
"node": ">=10.13.0"
|
| 3858 |
}
|
| 3859 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3860 |
"node_modules/error-ex": {
|
| 3861 |
"version": "1.3.4",
|
| 3862 |
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
|
|
@@ -3930,12 +4120,28 @@
|
|
| 3930 |
"url": "https://github.com/sponsors/sindresorhus"
|
| 3931 |
}
|
| 3932 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3933 |
"node_modules/eventemitter3": {
|
| 3934 |
"version": "4.0.7",
|
| 3935 |
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
| 3936 |
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
| 3937 |
"license": "MIT"
|
| 3938 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3939 |
"node_modules/fast-deep-equal": {
|
| 3940 |
"version": "3.1.3",
|
| 3941 |
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
|
@@ -4064,6 +4270,174 @@
|
|
| 4064 |
"node": ">= 0.4"
|
| 4065 |
}
|
| 4066 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4067 |
"node_modules/hoist-non-react-statics": {
|
| 4068 |
"version": "3.3.2",
|
| 4069 |
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
|
@@ -4079,6 +4453,16 @@
|
|
| 4079 |
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
| 4080 |
"license": "MIT"
|
| 4081 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4082 |
"node_modules/import-fresh": {
|
| 4083 |
"version": "3.3.1",
|
| 4084 |
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
|
@@ -4095,6 +4479,12 @@
|
|
| 4095 |
"url": "https://github.com/sponsors/sindresorhus"
|
| 4096 |
}
|
| 4097 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4098 |
"node_modules/input-otp": {
|
| 4099 |
"version": "1.4.2",
|
| 4100 |
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz",
|
|
@@ -4114,6 +4504,30 @@
|
|
| 4114 |
"node": ">=12"
|
| 4115 |
}
|
| 4116 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4117 |
"node_modules/is-arrayish": {
|
| 4118 |
"version": "0.2.1",
|
| 4119 |
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
|
@@ -4135,6 +4549,38 @@
|
|
| 4135 |
"url": "https://github.com/sponsors/ljharb"
|
| 4136 |
}
|
| 4137 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4138 |
"node_modules/jiti": {
|
| 4139 |
"version": "2.6.1",
|
| 4140 |
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
|
@@ -4191,6 +4637,22 @@
|
|
| 4191 |
"node": ">=6"
|
| 4192 |
}
|
| 4193 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4194 |
"node_modules/lightningcss": {
|
| 4195 |
"version": "1.30.1",
|
| 4196 |
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
|
@@ -4448,6 +4910,16 @@
|
|
| 4448 |
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
| 4449 |
"license": "MIT"
|
| 4450 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4451 |
"node_modules/loose-envify": {
|
| 4452 |
"version": "1.4.0",
|
| 4453 |
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
|
@@ -4489,32 +4961,925 @@
|
|
| 4489 |
"@jridgewell/sourcemap-codec": "^1.5.5"
|
| 4490 |
}
|
| 4491 |
},
|
| 4492 |
-
"node_modules/
|
| 4493 |
-
"version": "
|
| 4494 |
-
"resolved": "https://registry.npmjs.org/
|
| 4495 |
-
"integrity": "sha512-
|
| 4496 |
-
"
|
| 4497 |
-
"
|
| 4498 |
-
|
| 4499 |
-
"
|
| 4500 |
}
|
| 4501 |
},
|
| 4502 |
-
"node_modules/
|
| 4503 |
-
"version": "3.
|
| 4504 |
-
"resolved": "https://registry.npmjs.org/
|
| 4505 |
-
"integrity": "sha512-
|
| 4506 |
-
"dev": true,
|
| 4507 |
"license": "MIT",
|
| 4508 |
"dependencies": {
|
| 4509 |
-
"
|
|
|
|
|
|
|
|
|
|
| 4510 |
},
|
| 4511 |
-
"
|
| 4512 |
-
"
|
|
|
|
| 4513 |
}
|
| 4514 |
},
|
| 4515 |
-
"node_modules/
|
| 4516 |
-
"version": "
|
| 4517 |
-
"resolved": "https://registry.npmjs.org/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4518 |
"integrity": "sha512-Rc5E7oe2YZ72N//S3QXGzbnXgqNrTESv8KKxABR20q2FLch9gHLo0JLyYo2hZ238bZ9Gx6cWhj9VO0IgwbMjCw==",
|
| 4519 |
"license": "MIT",
|
| 4520 |
"dependencies": {
|
|
@@ -4616,6 +5981,31 @@
|
|
| 4616 |
"node": ">=6"
|
| 4617 |
}
|
| 4618 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4619 |
"node_modules/parse-json": {
|
| 4620 |
"version": "5.2.0",
|
| 4621 |
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
|
@@ -4634,6 +6024,18 @@
|
|
| 4634 |
"url": "https://github.com/sponsors/sindresorhus"
|
| 4635 |
}
|
| 4636 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4637 |
"node_modules/path-parse": {
|
| 4638 |
"version": "1.0.7",
|
| 4639 |
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
|
@@ -4714,6 +6116,16 @@
|
|
| 4714 |
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
| 4715 |
"license": "MIT"
|
| 4716 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4717 |
"node_modules/react": {
|
| 4718 |
"version": "18.3.1",
|
| 4719 |
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
|
@@ -4822,6 +6234,33 @@
|
|
| 4822 |
"integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==",
|
| 4823 |
"license": "MIT"
|
| 4824 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4825 |
"node_modules/react-popper": {
|
| 4826 |
"version": "2.3.0",
|
| 4827 |
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz",
|
|
@@ -5048,6 +6487,107 @@
|
|
| 5048 |
"@babel/runtime": "^7.9.2"
|
| 5049 |
}
|
| 5050 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5051 |
"node_modules/resize-observer-polyfill": {
|
| 5052 |
"version": "1.5.1",
|
| 5053 |
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
|
@@ -5183,12 +6723,54 @@
|
|
| 5183 |
"node": ">=0.10.0"
|
| 5184 |
}
|
| 5185 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5186 |
"node_modules/string-convert": {
|
| 5187 |
"version": "0.2.1",
|
| 5188 |
"resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
|
| 5189 |
"integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==",
|
| 5190 |
"license": "MIT"
|
| 5191 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5192 |
"node_modules/stylis": {
|
| 5193 |
"version": "4.2.0",
|
| 5194 |
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
|
|
@@ -5288,6 +6870,26 @@
|
|
| 5288 |
"url": "https://github.com/sponsors/SuperchupuDev"
|
| 5289 |
}
|
| 5290 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5291 |
"node_modules/tslib": {
|
| 5292 |
"version": "2.8.1",
|
| 5293 |
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
|
@@ -5303,6 +6905,121 @@
|
|
| 5303 |
"url": "https://github.com/sponsors/Wombosvideo"
|
| 5304 |
}
|
| 5305 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5306 |
"node_modules/update-browserslist-db": {
|
| 5307 |
"version": "1.2.3",
|
| 5308 |
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
|
@@ -5390,6 +7107,48 @@
|
|
| 5390 |
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
|
| 5391 |
}
|
| 5392 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5393 |
"node_modules/victory-vendor": {
|
| 5394 |
"version": "36.9.2",
|
| 5395 |
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
|
|
@@ -5496,6 +7255,16 @@
|
|
| 5496 |
"loose-envify": "^1.0.0"
|
| 5497 |
}
|
| 5498 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5499 |
"node_modules/yallist": {
|
| 5500 |
"version": "3.1.1",
|
| 5501 |
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
|
@@ -5520,6 +7289,16 @@
|
|
| 5520 |
"funding": {
|
| 5521 |
"url": "https://github.com/sponsors/eemeli"
|
| 5522 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5523 |
}
|
| 5524 |
}
|
| 5525 |
}
|
|
|
|
| 46 |
"date-fns": "3.6.0",
|
| 47 |
"embla-carousel-react": "8.6.0",
|
| 48 |
"input-otp": "1.4.2",
|
| 49 |
+
"katex": "^0.16.45",
|
| 50 |
"lucide-react": "0.487.0",
|
| 51 |
"motion": "12.23.24",
|
| 52 |
"next-themes": "0.4.6",
|
|
|
|
| 54 |
"react-dnd": "16.0.1",
|
| 55 |
"react-dnd-html5-backend": "16.0.1",
|
| 56 |
"react-hook-form": "7.55.0",
|
| 57 |
+
"react-markdown": "^10.1.0",
|
| 58 |
"react-popper": "2.3.0",
|
| 59 |
"react-resizable-panels": "2.1.7",
|
| 60 |
"react-responsive-masonry": "2.7.1",
|
| 61 |
"react-router": "7.13.0",
|
| 62 |
"react-slick": "0.31.0",
|
| 63 |
"recharts": "2.15.2",
|
| 64 |
+
"rehype-katex": "^7.0.1",
|
| 65 |
+
"remark-gfm": "^4.0.1",
|
| 66 |
+
"remark-math": "^6.0.0",
|
| 67 |
"sonner": "2.0.3",
|
| 68 |
"tailwind-merge": "3.2.0",
|
| 69 |
"tw-animate-css": "1.3.8",
|
|
|
|
| 3346 |
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
| 3347 |
"license": "MIT"
|
| 3348 |
},
|
| 3349 |
+
"node_modules/@types/debug": {
|
| 3350 |
+
"version": "4.1.13",
|
| 3351 |
+
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
|
| 3352 |
+
"integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",
|
| 3353 |
+
"license": "MIT",
|
| 3354 |
+
"dependencies": {
|
| 3355 |
+
"@types/ms": "*"
|
| 3356 |
+
}
|
| 3357 |
+
},
|
| 3358 |
"node_modules/@types/estree": {
|
| 3359 |
"version": "1.0.8",
|
| 3360 |
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
| 3361 |
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
| 3362 |
+
"license": "MIT"
|
| 3363 |
+
},
|
| 3364 |
+
"node_modules/@types/estree-jsx": {
|
| 3365 |
+
"version": "1.0.5",
|
| 3366 |
+
"resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
|
| 3367 |
+
"integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
|
| 3368 |
+
"license": "MIT",
|
| 3369 |
+
"dependencies": {
|
| 3370 |
+
"@types/estree": "*"
|
| 3371 |
+
}
|
| 3372 |
+
},
|
| 3373 |
+
"node_modules/@types/hast": {
|
| 3374 |
+
"version": "3.0.4",
|
| 3375 |
+
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
| 3376 |
+
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
|
| 3377 |
+
"license": "MIT",
|
| 3378 |
+
"dependencies": {
|
| 3379 |
+
"@types/unist": "*"
|
| 3380 |
+
}
|
| 3381 |
+
},
|
| 3382 |
+
"node_modules/@types/katex": {
|
| 3383 |
+
"version": "0.16.8",
|
| 3384 |
+
"resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz",
|
| 3385 |
+
"integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==",
|
| 3386 |
+
"license": "MIT"
|
| 3387 |
+
},
|
| 3388 |
+
"node_modules/@types/mdast": {
|
| 3389 |
+
"version": "4.0.4",
|
| 3390 |
+
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
|
| 3391 |
+
"integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
|
| 3392 |
+
"license": "MIT",
|
| 3393 |
+
"dependencies": {
|
| 3394 |
+
"@types/unist": "*"
|
| 3395 |
+
}
|
| 3396 |
+
},
|
| 3397 |
+
"node_modules/@types/ms": {
|
| 3398 |
+
"version": "2.1.0",
|
| 3399 |
+
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
| 3400 |
+
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
| 3401 |
"license": "MIT"
|
| 3402 |
},
|
| 3403 |
"node_modules/@types/parse-json": {
|
|
|
|
| 3431 |
"@types/react": "*"
|
| 3432 |
}
|
| 3433 |
},
|
| 3434 |
+
"node_modules/@types/unist": {
|
| 3435 |
+
"version": "3.0.3",
|
| 3436 |
+
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
| 3437 |
+
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
| 3438 |
+
"license": "MIT"
|
| 3439 |
+
},
|
| 3440 |
+
"node_modules/@ungap/structured-clone": {
|
| 3441 |
+
"version": "1.3.0",
|
| 3442 |
+
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
| 3443 |
+
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
|
| 3444 |
+
"license": "ISC"
|
| 3445 |
+
},
|
| 3446 |
"node_modules/@vitejs/plugin-react": {
|
| 3447 |
"version": "4.7.0",
|
| 3448 |
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
|
|
|
| 3491 |
"npm": ">=6"
|
| 3492 |
}
|
| 3493 |
},
|
| 3494 |
+
"node_modules/bail": {
|
| 3495 |
+
"version": "2.0.2",
|
| 3496 |
+
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
|
| 3497 |
+
"integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
|
| 3498 |
+
"license": "MIT",
|
| 3499 |
+
"funding": {
|
| 3500 |
+
"type": "github",
|
| 3501 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 3502 |
+
}
|
| 3503 |
+
},
|
| 3504 |
"node_modules/baseline-browser-mapping": {
|
| 3505 |
"version": "2.10.16",
|
| 3506 |
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz",
|
|
|
|
| 3588 |
"url": "https://www.paypal.me/kirilvatev"
|
| 3589 |
}
|
| 3590 |
},
|
| 3591 |
+
"node_modules/ccount": {
|
| 3592 |
+
"version": "2.0.1",
|
| 3593 |
+
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
|
| 3594 |
+
"integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
|
| 3595 |
+
"license": "MIT",
|
| 3596 |
+
"funding": {
|
| 3597 |
+
"type": "github",
|
| 3598 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 3599 |
+
}
|
| 3600 |
+
},
|
| 3601 |
+
"node_modules/character-entities": {
|
| 3602 |
+
"version": "2.0.2",
|
| 3603 |
+
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
|
| 3604 |
+
"integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
|
| 3605 |
+
"license": "MIT",
|
| 3606 |
+
"funding": {
|
| 3607 |
+
"type": "github",
|
| 3608 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 3609 |
+
}
|
| 3610 |
+
},
|
| 3611 |
+
"node_modules/character-entities-html4": {
|
| 3612 |
+
"version": "2.1.0",
|
| 3613 |
+
"resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
|
| 3614 |
+
"integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
|
| 3615 |
+
"license": "MIT",
|
| 3616 |
+
"funding": {
|
| 3617 |
+
"type": "github",
|
| 3618 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 3619 |
+
}
|
| 3620 |
+
},
|
| 3621 |
+
"node_modules/character-entities-legacy": {
|
| 3622 |
+
"version": "3.0.0",
|
| 3623 |
+
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
|
| 3624 |
+
"integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
|
| 3625 |
+
"license": "MIT",
|
| 3626 |
+
"funding": {
|
| 3627 |
+
"type": "github",
|
| 3628 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 3629 |
+
}
|
| 3630 |
+
},
|
| 3631 |
+
"node_modules/character-reference-invalid": {
|
| 3632 |
+
"version": "2.0.1",
|
| 3633 |
+
"resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
|
| 3634 |
+
"integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
|
| 3635 |
+
"license": "MIT",
|
| 3636 |
+
"funding": {
|
| 3637 |
+
"type": "github",
|
| 3638 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 3639 |
+
}
|
| 3640 |
+
},
|
| 3641 |
"node_modules/chownr": {
|
| 3642 |
"version": "3.0.0",
|
| 3643 |
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
|
|
|
| 3691 |
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
| 3692 |
}
|
| 3693 |
},
|
| 3694 |
+
"node_modules/comma-separated-tokens": {
|
| 3695 |
+
"version": "2.0.3",
|
| 3696 |
+
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
| 3697 |
+
"integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
|
| 3698 |
+
"license": "MIT",
|
| 3699 |
+
"funding": {
|
| 3700 |
+
"type": "github",
|
| 3701 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 3702 |
+
}
|
| 3703 |
+
},
|
| 3704 |
+
"node_modules/commander": {
|
| 3705 |
+
"version": "8.3.0",
|
| 3706 |
+
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
| 3707 |
+
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
|
| 3708 |
+
"license": "MIT",
|
| 3709 |
+
"engines": {
|
| 3710 |
+
"node": ">= 12"
|
| 3711 |
+
}
|
| 3712 |
+
},
|
| 3713 |
"node_modules/convert-source-map": {
|
| 3714 |
"version": "1.9.0",
|
| 3715 |
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
|
|
|
|
| 3914 |
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
| 3915 |
"license": "MIT"
|
| 3916 |
},
|
| 3917 |
+
"node_modules/decode-named-character-reference": {
|
| 3918 |
+
"version": "1.3.0",
|
| 3919 |
+
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
|
| 3920 |
+
"integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
|
| 3921 |
+
"license": "MIT",
|
| 3922 |
+
"dependencies": {
|
| 3923 |
+
"character-entities": "^2.0.0"
|
| 3924 |
+
},
|
| 3925 |
+
"funding": {
|
| 3926 |
+
"type": "github",
|
| 3927 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 3928 |
+
}
|
| 3929 |
+
},
|
| 3930 |
+
"node_modules/dequal": {
|
| 3931 |
+
"version": "2.0.3",
|
| 3932 |
+
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
| 3933 |
+
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
| 3934 |
+
"license": "MIT",
|
| 3935 |
+
"engines": {
|
| 3936 |
+
"node": ">=6"
|
| 3937 |
+
}
|
| 3938 |
+
},
|
| 3939 |
"node_modules/detect-libc": {
|
| 3940 |
"version": "2.1.2",
|
| 3941 |
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
|
|
|
| 3952 |
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
| 3953 |
"license": "MIT"
|
| 3954 |
},
|
| 3955 |
+
"node_modules/devlop": {
|
| 3956 |
+
"version": "1.1.0",
|
| 3957 |
+
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
|
| 3958 |
+
"integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
|
| 3959 |
+
"license": "MIT",
|
| 3960 |
+
"dependencies": {
|
| 3961 |
+
"dequal": "^2.0.0"
|
| 3962 |
+
},
|
| 3963 |
+
"funding": {
|
| 3964 |
+
"type": "github",
|
| 3965 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 3966 |
+
}
|
| 3967 |
+
},
|
| 3968 |
"node_modules/dnd-core": {
|
| 3969 |
"version": "16.0.1",
|
| 3970 |
"resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz",
|
|
|
|
| 4035 |
"node": ">=10.13.0"
|
| 4036 |
}
|
| 4037 |
},
|
| 4038 |
+
"node_modules/entities": {
|
| 4039 |
+
"version": "6.0.1",
|
| 4040 |
+
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
| 4041 |
+
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
| 4042 |
+
"license": "BSD-2-Clause",
|
| 4043 |
+
"engines": {
|
| 4044 |
+
"node": ">=0.12"
|
| 4045 |
+
},
|
| 4046 |
+
"funding": {
|
| 4047 |
+
"url": "https://github.com/fb55/entities?sponsor=1"
|
| 4048 |
+
}
|
| 4049 |
+
},
|
| 4050 |
"node_modules/error-ex": {
|
| 4051 |
"version": "1.3.4",
|
| 4052 |
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
|
|
|
|
| 4120 |
"url": "https://github.com/sponsors/sindresorhus"
|
| 4121 |
}
|
| 4122 |
},
|
| 4123 |
+
"node_modules/estree-util-is-identifier-name": {
|
| 4124 |
+
"version": "3.0.0",
|
| 4125 |
+
"resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
|
| 4126 |
+
"integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
|
| 4127 |
+
"license": "MIT",
|
| 4128 |
+
"funding": {
|
| 4129 |
+
"type": "opencollective",
|
| 4130 |
+
"url": "https://opencollective.com/unified"
|
| 4131 |
+
}
|
| 4132 |
+
},
|
| 4133 |
"node_modules/eventemitter3": {
|
| 4134 |
"version": "4.0.7",
|
| 4135 |
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
| 4136 |
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
| 4137 |
"license": "MIT"
|
| 4138 |
},
|
| 4139 |
+
"node_modules/extend": {
|
| 4140 |
+
"version": "3.0.2",
|
| 4141 |
+
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
| 4142 |
+
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
| 4143 |
+
"license": "MIT"
|
| 4144 |
+
},
|
| 4145 |
"node_modules/fast-deep-equal": {
|
| 4146 |
"version": "3.1.3",
|
| 4147 |
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
|
|
|
| 4270 |
"node": ">= 0.4"
|
| 4271 |
}
|
| 4272 |
},
|
| 4273 |
+
"node_modules/hast-util-from-dom": {
|
| 4274 |
+
"version": "5.0.1",
|
| 4275 |
+
"resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz",
|
| 4276 |
+
"integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==",
|
| 4277 |
+
"license": "ISC",
|
| 4278 |
+
"dependencies": {
|
| 4279 |
+
"@types/hast": "^3.0.0",
|
| 4280 |
+
"hastscript": "^9.0.0",
|
| 4281 |
+
"web-namespaces": "^2.0.0"
|
| 4282 |
+
},
|
| 4283 |
+
"funding": {
|
| 4284 |
+
"type": "opencollective",
|
| 4285 |
+
"url": "https://opencollective.com/unified"
|
| 4286 |
+
}
|
| 4287 |
+
},
|
| 4288 |
+
"node_modules/hast-util-from-html": {
|
| 4289 |
+
"version": "2.0.3",
|
| 4290 |
+
"resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz",
|
| 4291 |
+
"integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==",
|
| 4292 |
+
"license": "MIT",
|
| 4293 |
+
"dependencies": {
|
| 4294 |
+
"@types/hast": "^3.0.0",
|
| 4295 |
+
"devlop": "^1.1.0",
|
| 4296 |
+
"hast-util-from-parse5": "^8.0.0",
|
| 4297 |
+
"parse5": "^7.0.0",
|
| 4298 |
+
"vfile": "^6.0.0",
|
| 4299 |
+
"vfile-message": "^4.0.0"
|
| 4300 |
+
},
|
| 4301 |
+
"funding": {
|
| 4302 |
+
"type": "opencollective",
|
| 4303 |
+
"url": "https://opencollective.com/unified"
|
| 4304 |
+
}
|
| 4305 |
+
},
|
| 4306 |
+
"node_modules/hast-util-from-html-isomorphic": {
|
| 4307 |
+
"version": "2.0.0",
|
| 4308 |
+
"resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz",
|
| 4309 |
+
"integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==",
|
| 4310 |
+
"license": "MIT",
|
| 4311 |
+
"dependencies": {
|
| 4312 |
+
"@types/hast": "^3.0.0",
|
| 4313 |
+
"hast-util-from-dom": "^5.0.0",
|
| 4314 |
+
"hast-util-from-html": "^2.0.0",
|
| 4315 |
+
"unist-util-remove-position": "^5.0.0"
|
| 4316 |
+
},
|
| 4317 |
+
"funding": {
|
| 4318 |
+
"type": "opencollective",
|
| 4319 |
+
"url": "https://opencollective.com/unified"
|
| 4320 |
+
}
|
| 4321 |
+
},
|
| 4322 |
+
"node_modules/hast-util-from-parse5": {
|
| 4323 |
+
"version": "8.0.3",
|
| 4324 |
+
"resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz",
|
| 4325 |
+
"integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==",
|
| 4326 |
+
"license": "MIT",
|
| 4327 |
+
"dependencies": {
|
| 4328 |
+
"@types/hast": "^3.0.0",
|
| 4329 |
+
"@types/unist": "^3.0.0",
|
| 4330 |
+
"devlop": "^1.0.0",
|
| 4331 |
+
"hastscript": "^9.0.0",
|
| 4332 |
+
"property-information": "^7.0.0",
|
| 4333 |
+
"vfile": "^6.0.0",
|
| 4334 |
+
"vfile-location": "^5.0.0",
|
| 4335 |
+
"web-namespaces": "^2.0.0"
|
| 4336 |
+
},
|
| 4337 |
+
"funding": {
|
| 4338 |
+
"type": "opencollective",
|
| 4339 |
+
"url": "https://opencollective.com/unified"
|
| 4340 |
+
}
|
| 4341 |
+
},
|
| 4342 |
+
"node_modules/hast-util-is-element": {
|
| 4343 |
+
"version": "3.0.0",
|
| 4344 |
+
"resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz",
|
| 4345 |
+
"integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==",
|
| 4346 |
+
"license": "MIT",
|
| 4347 |
+
"dependencies": {
|
| 4348 |
+
"@types/hast": "^3.0.0"
|
| 4349 |
+
},
|
| 4350 |
+
"funding": {
|
| 4351 |
+
"type": "opencollective",
|
| 4352 |
+
"url": "https://opencollective.com/unified"
|
| 4353 |
+
}
|
| 4354 |
+
},
|
| 4355 |
+
"node_modules/hast-util-parse-selector": {
|
| 4356 |
+
"version": "4.0.0",
|
| 4357 |
+
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
|
| 4358 |
+
"integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
|
| 4359 |
+
"license": "MIT",
|
| 4360 |
+
"dependencies": {
|
| 4361 |
+
"@types/hast": "^3.0.0"
|
| 4362 |
+
},
|
| 4363 |
+
"funding": {
|
| 4364 |
+
"type": "opencollective",
|
| 4365 |
+
"url": "https://opencollective.com/unified"
|
| 4366 |
+
}
|
| 4367 |
+
},
|
| 4368 |
+
"node_modules/hast-util-to-jsx-runtime": {
|
| 4369 |
+
"version": "2.3.6",
|
| 4370 |
+
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
|
| 4371 |
+
"integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
|
| 4372 |
+
"license": "MIT",
|
| 4373 |
+
"dependencies": {
|
| 4374 |
+
"@types/estree": "^1.0.0",
|
| 4375 |
+
"@types/hast": "^3.0.0",
|
| 4376 |
+
"@types/unist": "^3.0.0",
|
| 4377 |
+
"comma-separated-tokens": "^2.0.0",
|
| 4378 |
+
"devlop": "^1.0.0",
|
| 4379 |
+
"estree-util-is-identifier-name": "^3.0.0",
|
| 4380 |
+
"hast-util-whitespace": "^3.0.0",
|
| 4381 |
+
"mdast-util-mdx-expression": "^2.0.0",
|
| 4382 |
+
"mdast-util-mdx-jsx": "^3.0.0",
|
| 4383 |
+
"mdast-util-mdxjs-esm": "^2.0.0",
|
| 4384 |
+
"property-information": "^7.0.0",
|
| 4385 |
+
"space-separated-tokens": "^2.0.0",
|
| 4386 |
+
"style-to-js": "^1.0.0",
|
| 4387 |
+
"unist-util-position": "^5.0.0",
|
| 4388 |
+
"vfile-message": "^4.0.0"
|
| 4389 |
+
},
|
| 4390 |
+
"funding": {
|
| 4391 |
+
"type": "opencollective",
|
| 4392 |
+
"url": "https://opencollective.com/unified"
|
| 4393 |
+
}
|
| 4394 |
+
},
|
| 4395 |
+
"node_modules/hast-util-to-text": {
|
| 4396 |
+
"version": "4.0.2",
|
| 4397 |
+
"resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz",
|
| 4398 |
+
"integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==",
|
| 4399 |
+
"license": "MIT",
|
| 4400 |
+
"dependencies": {
|
| 4401 |
+
"@types/hast": "^3.0.0",
|
| 4402 |
+
"@types/unist": "^3.0.0",
|
| 4403 |
+
"hast-util-is-element": "^3.0.0",
|
| 4404 |
+
"unist-util-find-after": "^5.0.0"
|
| 4405 |
+
},
|
| 4406 |
+
"funding": {
|
| 4407 |
+
"type": "opencollective",
|
| 4408 |
+
"url": "https://opencollective.com/unified"
|
| 4409 |
+
}
|
| 4410 |
+
},
|
| 4411 |
+
"node_modules/hast-util-whitespace": {
|
| 4412 |
+
"version": "3.0.0",
|
| 4413 |
+
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
|
| 4414 |
+
"integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
|
| 4415 |
+
"license": "MIT",
|
| 4416 |
+
"dependencies": {
|
| 4417 |
+
"@types/hast": "^3.0.0"
|
| 4418 |
+
},
|
| 4419 |
+
"funding": {
|
| 4420 |
+
"type": "opencollective",
|
| 4421 |
+
"url": "https://opencollective.com/unified"
|
| 4422 |
+
}
|
| 4423 |
+
},
|
| 4424 |
+
"node_modules/hastscript": {
|
| 4425 |
+
"version": "9.0.1",
|
| 4426 |
+
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz",
|
| 4427 |
+
"integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==",
|
| 4428 |
+
"license": "MIT",
|
| 4429 |
+
"dependencies": {
|
| 4430 |
+
"@types/hast": "^3.0.0",
|
| 4431 |
+
"comma-separated-tokens": "^2.0.0",
|
| 4432 |
+
"hast-util-parse-selector": "^4.0.0",
|
| 4433 |
+
"property-information": "^7.0.0",
|
| 4434 |
+
"space-separated-tokens": "^2.0.0"
|
| 4435 |
+
},
|
| 4436 |
+
"funding": {
|
| 4437 |
+
"type": "opencollective",
|
| 4438 |
+
"url": "https://opencollective.com/unified"
|
| 4439 |
+
}
|
| 4440 |
+
},
|
| 4441 |
"node_modules/hoist-non-react-statics": {
|
| 4442 |
"version": "3.3.2",
|
| 4443 |
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
|
|
|
| 4453 |
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
| 4454 |
"license": "MIT"
|
| 4455 |
},
|
| 4456 |
+
"node_modules/html-url-attributes": {
|
| 4457 |
+
"version": "3.0.1",
|
| 4458 |
+
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
|
| 4459 |
+
"integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
|
| 4460 |
+
"license": "MIT",
|
| 4461 |
+
"funding": {
|
| 4462 |
+
"type": "opencollective",
|
| 4463 |
+
"url": "https://opencollective.com/unified"
|
| 4464 |
+
}
|
| 4465 |
+
},
|
| 4466 |
"node_modules/import-fresh": {
|
| 4467 |
"version": "3.3.1",
|
| 4468 |
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
|
|
|
| 4479 |
"url": "https://github.com/sponsors/sindresorhus"
|
| 4480 |
}
|
| 4481 |
},
|
| 4482 |
+
"node_modules/inline-style-parser": {
|
| 4483 |
+
"version": "0.2.7",
|
| 4484 |
+
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
|
| 4485 |
+
"integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
|
| 4486 |
+
"license": "MIT"
|
| 4487 |
+
},
|
| 4488 |
"node_modules/input-otp": {
|
| 4489 |
"version": "1.4.2",
|
| 4490 |
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz",
|
|
|
|
| 4504 |
"node": ">=12"
|
| 4505 |
}
|
| 4506 |
},
|
| 4507 |
+
"node_modules/is-alphabetical": {
|
| 4508 |
+
"version": "2.0.1",
|
| 4509 |
+
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
|
| 4510 |
+
"integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
|
| 4511 |
+
"license": "MIT",
|
| 4512 |
+
"funding": {
|
| 4513 |
+
"type": "github",
|
| 4514 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 4515 |
+
}
|
| 4516 |
+
},
|
| 4517 |
+
"node_modules/is-alphanumerical": {
|
| 4518 |
+
"version": "2.0.1",
|
| 4519 |
+
"resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
|
| 4520 |
+
"integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
|
| 4521 |
+
"license": "MIT",
|
| 4522 |
+
"dependencies": {
|
| 4523 |
+
"is-alphabetical": "^2.0.0",
|
| 4524 |
+
"is-decimal": "^2.0.0"
|
| 4525 |
+
},
|
| 4526 |
+
"funding": {
|
| 4527 |
+
"type": "github",
|
| 4528 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 4529 |
+
}
|
| 4530 |
+
},
|
| 4531 |
"node_modules/is-arrayish": {
|
| 4532 |
"version": "0.2.1",
|
| 4533 |
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
|
|
|
| 4549 |
"url": "https://github.com/sponsors/ljharb"
|
| 4550 |
}
|
| 4551 |
},
|
| 4552 |
+
"node_modules/is-decimal": {
|
| 4553 |
+
"version": "2.0.1",
|
| 4554 |
+
"resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
|
| 4555 |
+
"integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
|
| 4556 |
+
"license": "MIT",
|
| 4557 |
+
"funding": {
|
| 4558 |
+
"type": "github",
|
| 4559 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 4560 |
+
}
|
| 4561 |
+
},
|
| 4562 |
+
"node_modules/is-hexadecimal": {
|
| 4563 |
+
"version": "2.0.1",
|
| 4564 |
+
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
|
| 4565 |
+
"integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
|
| 4566 |
+
"license": "MIT",
|
| 4567 |
+
"funding": {
|
| 4568 |
+
"type": "github",
|
| 4569 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 4570 |
+
}
|
| 4571 |
+
},
|
| 4572 |
+
"node_modules/is-plain-obj": {
|
| 4573 |
+
"version": "4.1.0",
|
| 4574 |
+
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
|
| 4575 |
+
"integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
|
| 4576 |
+
"license": "MIT",
|
| 4577 |
+
"engines": {
|
| 4578 |
+
"node": ">=12"
|
| 4579 |
+
},
|
| 4580 |
+
"funding": {
|
| 4581 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 4582 |
+
}
|
| 4583 |
+
},
|
| 4584 |
"node_modules/jiti": {
|
| 4585 |
"version": "2.6.1",
|
| 4586 |
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
|
|
|
| 4637 |
"node": ">=6"
|
| 4638 |
}
|
| 4639 |
},
|
| 4640 |
+
"node_modules/katex": {
|
| 4641 |
+
"version": "0.16.45",
|
| 4642 |
+
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz",
|
| 4643 |
+
"integrity": "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==",
|
| 4644 |
+
"funding": [
|
| 4645 |
+
"https://opencollective.com/katex",
|
| 4646 |
+
"https://github.com/sponsors/katex"
|
| 4647 |
+
],
|
| 4648 |
+
"license": "MIT",
|
| 4649 |
+
"dependencies": {
|
| 4650 |
+
"commander": "^8.3.0"
|
| 4651 |
+
},
|
| 4652 |
+
"bin": {
|
| 4653 |
+
"katex": "cli.js"
|
| 4654 |
+
}
|
| 4655 |
+
},
|
| 4656 |
"node_modules/lightningcss": {
|
| 4657 |
"version": "1.30.1",
|
| 4658 |
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
|
|
|
| 4910 |
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
| 4911 |
"license": "MIT"
|
| 4912 |
},
|
| 4913 |
+
"node_modules/longest-streak": {
|
| 4914 |
+
"version": "3.1.0",
|
| 4915 |
+
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
|
| 4916 |
+
"integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
|
| 4917 |
+
"license": "MIT",
|
| 4918 |
+
"funding": {
|
| 4919 |
+
"type": "github",
|
| 4920 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 4921 |
+
}
|
| 4922 |
+
},
|
| 4923 |
"node_modules/loose-envify": {
|
| 4924 |
"version": "1.4.0",
|
| 4925 |
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
|
|
|
| 4961 |
"@jridgewell/sourcemap-codec": "^1.5.5"
|
| 4962 |
}
|
| 4963 |
},
|
| 4964 |
+
"node_modules/markdown-table": {
|
| 4965 |
+
"version": "3.0.4",
|
| 4966 |
+
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
|
| 4967 |
+
"integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
|
| 4968 |
+
"license": "MIT",
|
| 4969 |
+
"funding": {
|
| 4970 |
+
"type": "github",
|
| 4971 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 4972 |
}
|
| 4973 |
},
|
| 4974 |
+
"node_modules/mdast-util-find-and-replace": {
|
| 4975 |
+
"version": "3.0.2",
|
| 4976 |
+
"resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
|
| 4977 |
+
"integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
|
|
|
|
| 4978 |
"license": "MIT",
|
| 4979 |
"dependencies": {
|
| 4980 |
+
"@types/mdast": "^4.0.0",
|
| 4981 |
+
"escape-string-regexp": "^5.0.0",
|
| 4982 |
+
"unist-util-is": "^6.0.0",
|
| 4983 |
+
"unist-util-visit-parents": "^6.0.0"
|
| 4984 |
},
|
| 4985 |
+
"funding": {
|
| 4986 |
+
"type": "opencollective",
|
| 4987 |
+
"url": "https://opencollective.com/unified"
|
| 4988 |
}
|
| 4989 |
},
|
| 4990 |
+
"node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": {
|
| 4991 |
+
"version": "5.0.0",
|
| 4992 |
+
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
|
| 4993 |
+
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
|
| 4994 |
+
"license": "MIT",
|
| 4995 |
+
"engines": {
|
| 4996 |
+
"node": ">=12"
|
| 4997 |
+
},
|
| 4998 |
+
"funding": {
|
| 4999 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 5000 |
+
}
|
| 5001 |
+
},
|
| 5002 |
+
"node_modules/mdast-util-from-markdown": {
|
| 5003 |
+
"version": "2.0.3",
|
| 5004 |
+
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",
|
| 5005 |
+
"integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==",
|
| 5006 |
+
"license": "MIT",
|
| 5007 |
+
"dependencies": {
|
| 5008 |
+
"@types/mdast": "^4.0.0",
|
| 5009 |
+
"@types/unist": "^3.0.0",
|
| 5010 |
+
"decode-named-character-reference": "^1.0.0",
|
| 5011 |
+
"devlop": "^1.0.0",
|
| 5012 |
+
"mdast-util-to-string": "^4.0.0",
|
| 5013 |
+
"micromark": "^4.0.0",
|
| 5014 |
+
"micromark-util-decode-numeric-character-reference": "^2.0.0",
|
| 5015 |
+
"micromark-util-decode-string": "^2.0.0",
|
| 5016 |
+
"micromark-util-normalize-identifier": "^2.0.0",
|
| 5017 |
+
"micromark-util-symbol": "^2.0.0",
|
| 5018 |
+
"micromark-util-types": "^2.0.0",
|
| 5019 |
+
"unist-util-stringify-position": "^4.0.0"
|
| 5020 |
+
},
|
| 5021 |
+
"funding": {
|
| 5022 |
+
"type": "opencollective",
|
| 5023 |
+
"url": "https://opencollective.com/unified"
|
| 5024 |
+
}
|
| 5025 |
+
},
|
| 5026 |
+
"node_modules/mdast-util-gfm": {
|
| 5027 |
+
"version": "3.1.0",
|
| 5028 |
+
"resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
|
| 5029 |
+
"integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
|
| 5030 |
+
"license": "MIT",
|
| 5031 |
+
"dependencies": {
|
| 5032 |
+
"mdast-util-from-markdown": "^2.0.0",
|
| 5033 |
+
"mdast-util-gfm-autolink-literal": "^2.0.0",
|
| 5034 |
+
"mdast-util-gfm-footnote": "^2.0.0",
|
| 5035 |
+
"mdast-util-gfm-strikethrough": "^2.0.0",
|
| 5036 |
+
"mdast-util-gfm-table": "^2.0.0",
|
| 5037 |
+
"mdast-util-gfm-task-list-item": "^2.0.0",
|
| 5038 |
+
"mdast-util-to-markdown": "^2.0.0"
|
| 5039 |
+
},
|
| 5040 |
+
"funding": {
|
| 5041 |
+
"type": "opencollective",
|
| 5042 |
+
"url": "https://opencollective.com/unified"
|
| 5043 |
+
}
|
| 5044 |
+
},
|
| 5045 |
+
"node_modules/mdast-util-gfm-autolink-literal": {
|
| 5046 |
+
"version": "2.0.1",
|
| 5047 |
+
"resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
|
| 5048 |
+
"integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
|
| 5049 |
+
"license": "MIT",
|
| 5050 |
+
"dependencies": {
|
| 5051 |
+
"@types/mdast": "^4.0.0",
|
| 5052 |
+
"ccount": "^2.0.0",
|
| 5053 |
+
"devlop": "^1.0.0",
|
| 5054 |
+
"mdast-util-find-and-replace": "^3.0.0",
|
| 5055 |
+
"micromark-util-character": "^2.0.0"
|
| 5056 |
+
},
|
| 5057 |
+
"funding": {
|
| 5058 |
+
"type": "opencollective",
|
| 5059 |
+
"url": "https://opencollective.com/unified"
|
| 5060 |
+
}
|
| 5061 |
+
},
|
| 5062 |
+
"node_modules/mdast-util-gfm-footnote": {
|
| 5063 |
+
"version": "2.1.0",
|
| 5064 |
+
"resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
|
| 5065 |
+
"integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
|
| 5066 |
+
"license": "MIT",
|
| 5067 |
+
"dependencies": {
|
| 5068 |
+
"@types/mdast": "^4.0.0",
|
| 5069 |
+
"devlop": "^1.1.0",
|
| 5070 |
+
"mdast-util-from-markdown": "^2.0.0",
|
| 5071 |
+
"mdast-util-to-markdown": "^2.0.0",
|
| 5072 |
+
"micromark-util-normalize-identifier": "^2.0.0"
|
| 5073 |
+
},
|
| 5074 |
+
"funding": {
|
| 5075 |
+
"type": "opencollective",
|
| 5076 |
+
"url": "https://opencollective.com/unified"
|
| 5077 |
+
}
|
| 5078 |
+
},
|
| 5079 |
+
"node_modules/mdast-util-gfm-strikethrough": {
|
| 5080 |
+
"version": "2.0.0",
|
| 5081 |
+
"resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
|
| 5082 |
+
"integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
|
| 5083 |
+
"license": "MIT",
|
| 5084 |
+
"dependencies": {
|
| 5085 |
+
"@types/mdast": "^4.0.0",
|
| 5086 |
+
"mdast-util-from-markdown": "^2.0.0",
|
| 5087 |
+
"mdast-util-to-markdown": "^2.0.0"
|
| 5088 |
+
},
|
| 5089 |
+
"funding": {
|
| 5090 |
+
"type": "opencollective",
|
| 5091 |
+
"url": "https://opencollective.com/unified"
|
| 5092 |
+
}
|
| 5093 |
+
},
|
| 5094 |
+
"node_modules/mdast-util-gfm-table": {
|
| 5095 |
+
"version": "2.0.0",
|
| 5096 |
+
"resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
|
| 5097 |
+
"integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
|
| 5098 |
+
"license": "MIT",
|
| 5099 |
+
"dependencies": {
|
| 5100 |
+
"@types/mdast": "^4.0.0",
|
| 5101 |
+
"devlop": "^1.0.0",
|
| 5102 |
+
"markdown-table": "^3.0.0",
|
| 5103 |
+
"mdast-util-from-markdown": "^2.0.0",
|
| 5104 |
+
"mdast-util-to-markdown": "^2.0.0"
|
| 5105 |
+
},
|
| 5106 |
+
"funding": {
|
| 5107 |
+
"type": "opencollective",
|
| 5108 |
+
"url": "https://opencollective.com/unified"
|
| 5109 |
+
}
|
| 5110 |
+
},
|
| 5111 |
+
"node_modules/mdast-util-gfm-task-list-item": {
|
| 5112 |
+
"version": "2.0.0",
|
| 5113 |
+
"resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
|
| 5114 |
+
"integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
|
| 5115 |
+
"license": "MIT",
|
| 5116 |
+
"dependencies": {
|
| 5117 |
+
"@types/mdast": "^4.0.0",
|
| 5118 |
+
"devlop": "^1.0.0",
|
| 5119 |
+
"mdast-util-from-markdown": "^2.0.0",
|
| 5120 |
+
"mdast-util-to-markdown": "^2.0.0"
|
| 5121 |
+
},
|
| 5122 |
+
"funding": {
|
| 5123 |
+
"type": "opencollective",
|
| 5124 |
+
"url": "https://opencollective.com/unified"
|
| 5125 |
+
}
|
| 5126 |
+
},
|
| 5127 |
+
"node_modules/mdast-util-math": {
|
| 5128 |
+
"version": "3.0.0",
|
| 5129 |
+
"resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz",
|
| 5130 |
+
"integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==",
|
| 5131 |
+
"license": "MIT",
|
| 5132 |
+
"dependencies": {
|
| 5133 |
+
"@types/hast": "^3.0.0",
|
| 5134 |
+
"@types/mdast": "^4.0.0",
|
| 5135 |
+
"devlop": "^1.0.0",
|
| 5136 |
+
"longest-streak": "^3.0.0",
|
| 5137 |
+
"mdast-util-from-markdown": "^2.0.0",
|
| 5138 |
+
"mdast-util-to-markdown": "^2.1.0",
|
| 5139 |
+
"unist-util-remove-position": "^5.0.0"
|
| 5140 |
+
},
|
| 5141 |
+
"funding": {
|
| 5142 |
+
"type": "opencollective",
|
| 5143 |
+
"url": "https://opencollective.com/unified"
|
| 5144 |
+
}
|
| 5145 |
+
},
|
| 5146 |
+
"node_modules/mdast-util-mdx-expression": {
|
| 5147 |
+
"version": "2.0.1",
|
| 5148 |
+
"resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
|
| 5149 |
+
"integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
|
| 5150 |
+
"license": "MIT",
|
| 5151 |
+
"dependencies": {
|
| 5152 |
+
"@types/estree-jsx": "^1.0.0",
|
| 5153 |
+
"@types/hast": "^3.0.0",
|
| 5154 |
+
"@types/mdast": "^4.0.0",
|
| 5155 |
+
"devlop": "^1.0.0",
|
| 5156 |
+
"mdast-util-from-markdown": "^2.0.0",
|
| 5157 |
+
"mdast-util-to-markdown": "^2.0.0"
|
| 5158 |
+
},
|
| 5159 |
+
"funding": {
|
| 5160 |
+
"type": "opencollective",
|
| 5161 |
+
"url": "https://opencollective.com/unified"
|
| 5162 |
+
}
|
| 5163 |
+
},
|
| 5164 |
+
"node_modules/mdast-util-mdx-jsx": {
|
| 5165 |
+
"version": "3.2.0",
|
| 5166 |
+
"resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
|
| 5167 |
+
"integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
|
| 5168 |
+
"license": "MIT",
|
| 5169 |
+
"dependencies": {
|
| 5170 |
+
"@types/estree-jsx": "^1.0.0",
|
| 5171 |
+
"@types/hast": "^3.0.0",
|
| 5172 |
+
"@types/mdast": "^4.0.0",
|
| 5173 |
+
"@types/unist": "^3.0.0",
|
| 5174 |
+
"ccount": "^2.0.0",
|
| 5175 |
+
"devlop": "^1.1.0",
|
| 5176 |
+
"mdast-util-from-markdown": "^2.0.0",
|
| 5177 |
+
"mdast-util-to-markdown": "^2.0.0",
|
| 5178 |
+
"parse-entities": "^4.0.0",
|
| 5179 |
+
"stringify-entities": "^4.0.0",
|
| 5180 |
+
"unist-util-stringify-position": "^4.0.0",
|
| 5181 |
+
"vfile-message": "^4.0.0"
|
| 5182 |
+
},
|
| 5183 |
+
"funding": {
|
| 5184 |
+
"type": "opencollective",
|
| 5185 |
+
"url": "https://opencollective.com/unified"
|
| 5186 |
+
}
|
| 5187 |
+
},
|
| 5188 |
+
"node_modules/mdast-util-mdxjs-esm": {
|
| 5189 |
+
"version": "2.0.1",
|
| 5190 |
+
"resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
|
| 5191 |
+
"integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
|
| 5192 |
+
"license": "MIT",
|
| 5193 |
+
"dependencies": {
|
| 5194 |
+
"@types/estree-jsx": "^1.0.0",
|
| 5195 |
+
"@types/hast": "^3.0.0",
|
| 5196 |
+
"@types/mdast": "^4.0.0",
|
| 5197 |
+
"devlop": "^1.0.0",
|
| 5198 |
+
"mdast-util-from-markdown": "^2.0.0",
|
| 5199 |
+
"mdast-util-to-markdown": "^2.0.0"
|
| 5200 |
+
},
|
| 5201 |
+
"funding": {
|
| 5202 |
+
"type": "opencollective",
|
| 5203 |
+
"url": "https://opencollective.com/unified"
|
| 5204 |
+
}
|
| 5205 |
+
},
|
| 5206 |
+
"node_modules/mdast-util-phrasing": {
|
| 5207 |
+
"version": "4.1.0",
|
| 5208 |
+
"resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
|
| 5209 |
+
"integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
|
| 5210 |
+
"license": "MIT",
|
| 5211 |
+
"dependencies": {
|
| 5212 |
+
"@types/mdast": "^4.0.0",
|
| 5213 |
+
"unist-util-is": "^6.0.0"
|
| 5214 |
+
},
|
| 5215 |
+
"funding": {
|
| 5216 |
+
"type": "opencollective",
|
| 5217 |
+
"url": "https://opencollective.com/unified"
|
| 5218 |
+
}
|
| 5219 |
+
},
|
| 5220 |
+
"node_modules/mdast-util-to-hast": {
|
| 5221 |
+
"version": "13.2.1",
|
| 5222 |
+
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
|
| 5223 |
+
"integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
|
| 5224 |
+
"license": "MIT",
|
| 5225 |
+
"dependencies": {
|
| 5226 |
+
"@types/hast": "^3.0.0",
|
| 5227 |
+
"@types/mdast": "^4.0.0",
|
| 5228 |
+
"@ungap/structured-clone": "^1.0.0",
|
| 5229 |
+
"devlop": "^1.0.0",
|
| 5230 |
+
"micromark-util-sanitize-uri": "^2.0.0",
|
| 5231 |
+
"trim-lines": "^3.0.0",
|
| 5232 |
+
"unist-util-position": "^5.0.0",
|
| 5233 |
+
"unist-util-visit": "^5.0.0",
|
| 5234 |
+
"vfile": "^6.0.0"
|
| 5235 |
+
},
|
| 5236 |
+
"funding": {
|
| 5237 |
+
"type": "opencollective",
|
| 5238 |
+
"url": "https://opencollective.com/unified"
|
| 5239 |
+
}
|
| 5240 |
+
},
|
| 5241 |
+
"node_modules/mdast-util-to-markdown": {
|
| 5242 |
+
"version": "2.1.2",
|
| 5243 |
+
"resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
|
| 5244 |
+
"integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
|
| 5245 |
+
"license": "MIT",
|
| 5246 |
+
"dependencies": {
|
| 5247 |
+
"@types/mdast": "^4.0.0",
|
| 5248 |
+
"@types/unist": "^3.0.0",
|
| 5249 |
+
"longest-streak": "^3.0.0",
|
| 5250 |
+
"mdast-util-phrasing": "^4.0.0",
|
| 5251 |
+
"mdast-util-to-string": "^4.0.0",
|
| 5252 |
+
"micromark-util-classify-character": "^2.0.0",
|
| 5253 |
+
"micromark-util-decode-string": "^2.0.0",
|
| 5254 |
+
"unist-util-visit": "^5.0.0",
|
| 5255 |
+
"zwitch": "^2.0.0"
|
| 5256 |
+
},
|
| 5257 |
+
"funding": {
|
| 5258 |
+
"type": "opencollective",
|
| 5259 |
+
"url": "https://opencollective.com/unified"
|
| 5260 |
+
}
|
| 5261 |
+
},
|
| 5262 |
+
"node_modules/mdast-util-to-string": {
|
| 5263 |
+
"version": "4.0.0",
|
| 5264 |
+
"resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
|
| 5265 |
+
"integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
|
| 5266 |
+
"license": "MIT",
|
| 5267 |
+
"dependencies": {
|
| 5268 |
+
"@types/mdast": "^4.0.0"
|
| 5269 |
+
},
|
| 5270 |
+
"funding": {
|
| 5271 |
+
"type": "opencollective",
|
| 5272 |
+
"url": "https://opencollective.com/unified"
|
| 5273 |
+
}
|
| 5274 |
+
},
|
| 5275 |
+
"node_modules/micromark": {
|
| 5276 |
+
"version": "4.0.2",
|
| 5277 |
+
"resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
|
| 5278 |
+
"integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
|
| 5279 |
+
"funding": [
|
| 5280 |
+
{
|
| 5281 |
+
"type": "GitHub Sponsors",
|
| 5282 |
+
"url": "https://github.com/sponsors/unifiedjs"
|
| 5283 |
+
},
|
| 5284 |
+
{
|
| 5285 |
+
"type": "OpenCollective",
|
| 5286 |
+
"url": "https://opencollective.com/unified"
|
| 5287 |
+
}
|
| 5288 |
+
],
|
| 5289 |
+
"license": "MIT",
|
| 5290 |
+
"dependencies": {
|
| 5291 |
+
"@types/debug": "^4.0.0",
|
| 5292 |
+
"debug": "^4.0.0",
|
| 5293 |
+
"decode-named-character-reference": "^1.0.0",
|
| 5294 |
+
"devlop": "^1.0.0",
|
| 5295 |
+
"micromark-core-commonmark": "^2.0.0",
|
| 5296 |
+
"micromark-factory-space": "^2.0.0",
|
| 5297 |
+
"micromark-util-character": "^2.0.0",
|
| 5298 |
+
"micromark-util-chunked": "^2.0.0",
|
| 5299 |
+
"micromark-util-combine-extensions": "^2.0.0",
|
| 5300 |
+
"micromark-util-decode-numeric-character-reference": "^2.0.0",
|
| 5301 |
+
"micromark-util-encode": "^2.0.0",
|
| 5302 |
+
"micromark-util-normalize-identifier": "^2.0.0",
|
| 5303 |
+
"micromark-util-resolve-all": "^2.0.0",
|
| 5304 |
+
"micromark-util-sanitize-uri": "^2.0.0",
|
| 5305 |
+
"micromark-util-subtokenize": "^2.0.0",
|
| 5306 |
+
"micromark-util-symbol": "^2.0.0",
|
| 5307 |
+
"micromark-util-types": "^2.0.0"
|
| 5308 |
+
}
|
| 5309 |
+
},
|
| 5310 |
+
"node_modules/micromark-core-commonmark": {
|
| 5311 |
+
"version": "2.0.3",
|
| 5312 |
+
"resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
|
| 5313 |
+
"integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
|
| 5314 |
+
"funding": [
|
| 5315 |
+
{
|
| 5316 |
+
"type": "GitHub Sponsors",
|
| 5317 |
+
"url": "https://github.com/sponsors/unifiedjs"
|
| 5318 |
+
},
|
| 5319 |
+
{
|
| 5320 |
+
"type": "OpenCollective",
|
| 5321 |
+
"url": "https://opencollective.com/unified"
|
| 5322 |
+
}
|
| 5323 |
+
],
|
| 5324 |
+
"license": "MIT",
|
| 5325 |
+
"dependencies": {
|
| 5326 |
+
"decode-named-character-reference": "^1.0.0",
|
| 5327 |
+
"devlop": "^1.0.0",
|
| 5328 |
+
"micromark-factory-destination": "^2.0.0",
|
| 5329 |
+
"micromark-factory-label": "^2.0.0",
|
| 5330 |
+
"micromark-factory-space": "^2.0.0",
|
| 5331 |
+
"micromark-factory-title": "^2.0.0",
|
| 5332 |
+
"micromark-factory-whitespace": "^2.0.0",
|
| 5333 |
+
"micromark-util-character": "^2.0.0",
|
| 5334 |
+
"micromark-util-chunked": "^2.0.0",
|
| 5335 |
+
"micromark-util-classify-character": "^2.0.0",
|
| 5336 |
+
"micromark-util-html-tag-name": "^2.0.0",
|
| 5337 |
+
"micromark-util-normalize-identifier": "^2.0.0",
|
| 5338 |
+
"micromark-util-resolve-all": "^2.0.0",
|
| 5339 |
+
"micromark-util-subtokenize": "^2.0.0",
|
| 5340 |
+
"micromark-util-symbol": "^2.0.0",
|
| 5341 |
+
"micromark-util-types": "^2.0.0"
|
| 5342 |
+
}
|
| 5343 |
+
},
|
| 5344 |
+
"node_modules/micromark-extension-gfm": {
|
| 5345 |
+
"version": "3.0.0",
|
| 5346 |
+
"resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
|
| 5347 |
+
"integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
|
| 5348 |
+
"license": "MIT",
|
| 5349 |
+
"dependencies": {
|
| 5350 |
+
"micromark-extension-gfm-autolink-literal": "^2.0.0",
|
| 5351 |
+
"micromark-extension-gfm-footnote": "^2.0.0",
|
| 5352 |
+
"micromark-extension-gfm-strikethrough": "^2.0.0",
|
| 5353 |
+
"micromark-extension-gfm-table": "^2.0.0",
|
| 5354 |
+
"micromark-extension-gfm-tagfilter": "^2.0.0",
|
| 5355 |
+
"micromark-extension-gfm-task-list-item": "^2.0.0",
|
| 5356 |
+
"micromark-util-combine-extensions": "^2.0.0",
|
| 5357 |
+
"micromark-util-types": "^2.0.0"
|
| 5358 |
+
},
|
| 5359 |
+
"funding": {
|
| 5360 |
+
"type": "opencollective",
|
| 5361 |
+
"url": "https://opencollective.com/unified"
|
| 5362 |
+
}
|
| 5363 |
+
},
|
| 5364 |
+
"node_modules/micromark-extension-gfm-autolink-literal": {
|
| 5365 |
+
"version": "2.1.0",
|
| 5366 |
+
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
|
| 5367 |
+
"integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
|
| 5368 |
+
"license": "MIT",
|
| 5369 |
+
"dependencies": {
|
| 5370 |
+
"micromark-util-character": "^2.0.0",
|
| 5371 |
+
"micromark-util-sanitize-uri": "^2.0.0",
|
| 5372 |
+
"micromark-util-symbol": "^2.0.0",
|
| 5373 |
+
"micromark-util-types": "^2.0.0"
|
| 5374 |
+
},
|
| 5375 |
+
"funding": {
|
| 5376 |
+
"type": "opencollective",
|
| 5377 |
+
"url": "https://opencollective.com/unified"
|
| 5378 |
+
}
|
| 5379 |
+
},
|
| 5380 |
+
"node_modules/micromark-extension-gfm-footnote": {
|
| 5381 |
+
"version": "2.1.0",
|
| 5382 |
+
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
|
| 5383 |
+
"integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
|
| 5384 |
+
"license": "MIT",
|
| 5385 |
+
"dependencies": {
|
| 5386 |
+
"devlop": "^1.0.0",
|
| 5387 |
+
"micromark-core-commonmark": "^2.0.0",
|
| 5388 |
+
"micromark-factory-space": "^2.0.0",
|
| 5389 |
+
"micromark-util-character": "^2.0.0",
|
| 5390 |
+
"micromark-util-normalize-identifier": "^2.0.0",
|
| 5391 |
+
"micromark-util-sanitize-uri": "^2.0.0",
|
| 5392 |
+
"micromark-util-symbol": "^2.0.0",
|
| 5393 |
+
"micromark-util-types": "^2.0.0"
|
| 5394 |
+
},
|
| 5395 |
+
"funding": {
|
| 5396 |
+
"type": "opencollective",
|
| 5397 |
+
"url": "https://opencollective.com/unified"
|
| 5398 |
+
}
|
| 5399 |
+
},
|
| 5400 |
+
"node_modules/micromark-extension-gfm-strikethrough": {
|
| 5401 |
+
"version": "2.1.0",
|
| 5402 |
+
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
|
| 5403 |
+
"integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
|
| 5404 |
+
"license": "MIT",
|
| 5405 |
+
"dependencies": {
|
| 5406 |
+
"devlop": "^1.0.0",
|
| 5407 |
+
"micromark-util-chunked": "^2.0.0",
|
| 5408 |
+
"micromark-util-classify-character": "^2.0.0",
|
| 5409 |
+
"micromark-util-resolve-all": "^2.0.0",
|
| 5410 |
+
"micromark-util-symbol": "^2.0.0",
|
| 5411 |
+
"micromark-util-types": "^2.0.0"
|
| 5412 |
+
},
|
| 5413 |
+
"funding": {
|
| 5414 |
+
"type": "opencollective",
|
| 5415 |
+
"url": "https://opencollective.com/unified"
|
| 5416 |
+
}
|
| 5417 |
+
},
|
| 5418 |
+
"node_modules/micromark-extension-gfm-table": {
|
| 5419 |
+
"version": "2.1.1",
|
| 5420 |
+
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
|
| 5421 |
+
"integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
|
| 5422 |
+
"license": "MIT",
|
| 5423 |
+
"dependencies": {
|
| 5424 |
+
"devlop": "^1.0.0",
|
| 5425 |
+
"micromark-factory-space": "^2.0.0",
|
| 5426 |
+
"micromark-util-character": "^2.0.0",
|
| 5427 |
+
"micromark-util-symbol": "^2.0.0",
|
| 5428 |
+
"micromark-util-types": "^2.0.0"
|
| 5429 |
+
},
|
| 5430 |
+
"funding": {
|
| 5431 |
+
"type": "opencollective",
|
| 5432 |
+
"url": "https://opencollective.com/unified"
|
| 5433 |
+
}
|
| 5434 |
+
},
|
| 5435 |
+
"node_modules/micromark-extension-gfm-tagfilter": {
|
| 5436 |
+
"version": "2.0.0",
|
| 5437 |
+
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
|
| 5438 |
+
"integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
|
| 5439 |
+
"license": "MIT",
|
| 5440 |
+
"dependencies": {
|
| 5441 |
+
"micromark-util-types": "^2.0.0"
|
| 5442 |
+
},
|
| 5443 |
+
"funding": {
|
| 5444 |
+
"type": "opencollective",
|
| 5445 |
+
"url": "https://opencollective.com/unified"
|
| 5446 |
+
}
|
| 5447 |
+
},
|
| 5448 |
+
"node_modules/micromark-extension-gfm-task-list-item": {
|
| 5449 |
+
"version": "2.1.0",
|
| 5450 |
+
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
|
| 5451 |
+
"integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
|
| 5452 |
+
"license": "MIT",
|
| 5453 |
+
"dependencies": {
|
| 5454 |
+
"devlop": "^1.0.0",
|
| 5455 |
+
"micromark-factory-space": "^2.0.0",
|
| 5456 |
+
"micromark-util-character": "^2.0.0",
|
| 5457 |
+
"micromark-util-symbol": "^2.0.0",
|
| 5458 |
+
"micromark-util-types": "^2.0.0"
|
| 5459 |
+
},
|
| 5460 |
+
"funding": {
|
| 5461 |
+
"type": "opencollective",
|
| 5462 |
+
"url": "https://opencollective.com/unified"
|
| 5463 |
+
}
|
| 5464 |
+
},
|
| 5465 |
+
"node_modules/micromark-extension-math": {
|
| 5466 |
+
"version": "3.1.0",
|
| 5467 |
+
"resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz",
|
| 5468 |
+
"integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==",
|
| 5469 |
+
"license": "MIT",
|
| 5470 |
+
"dependencies": {
|
| 5471 |
+
"@types/katex": "^0.16.0",
|
| 5472 |
+
"devlop": "^1.0.0",
|
| 5473 |
+
"katex": "^0.16.0",
|
| 5474 |
+
"micromark-factory-space": "^2.0.0",
|
| 5475 |
+
"micromark-util-character": "^2.0.0",
|
| 5476 |
+
"micromark-util-symbol": "^2.0.0",
|
| 5477 |
+
"micromark-util-types": "^2.0.0"
|
| 5478 |
+
},
|
| 5479 |
+
"funding": {
|
| 5480 |
+
"type": "opencollective",
|
| 5481 |
+
"url": "https://opencollective.com/unified"
|
| 5482 |
+
}
|
| 5483 |
+
},
|
| 5484 |
+
"node_modules/micromark-factory-destination": {
|
| 5485 |
+
"version": "2.0.1",
|
| 5486 |
+
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
|
| 5487 |
+
"integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
|
| 5488 |
+
"funding": [
|
| 5489 |
+
{
|
| 5490 |
+
"type": "GitHub Sponsors",
|
| 5491 |
+
"url": "https://github.com/sponsors/unifiedjs"
|
| 5492 |
+
},
|
| 5493 |
+
{
|
| 5494 |
+
"type": "OpenCollective",
|
| 5495 |
+
"url": "https://opencollective.com/unified"
|
| 5496 |
+
}
|
| 5497 |
+
],
|
| 5498 |
+
"license": "MIT",
|
| 5499 |
+
"dependencies": {
|
| 5500 |
+
"micromark-util-character": "^2.0.0",
|
| 5501 |
+
"micromark-util-symbol": "^2.0.0",
|
| 5502 |
+
"micromark-util-types": "^2.0.0"
|
| 5503 |
+
}
|
| 5504 |
+
},
|
| 5505 |
+
"node_modules/micromark-factory-label": {
|
| 5506 |
+
"version": "2.0.1",
|
| 5507 |
+
"resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
|
| 5508 |
+
"integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
|
| 5509 |
+
"funding": [
|
| 5510 |
+
{
|
| 5511 |
+
"type": "GitHub Sponsors",
|
| 5512 |
+
"url": "https://github.com/sponsors/unifiedjs"
|
| 5513 |
+
},
|
| 5514 |
+
{
|
| 5515 |
+
"type": "OpenCollective",
|
| 5516 |
+
"url": "https://opencollective.com/unified"
|
| 5517 |
+
}
|
| 5518 |
+
],
|
| 5519 |
+
"license": "MIT",
|
| 5520 |
+
"dependencies": {
|
| 5521 |
+
"devlop": "^1.0.0",
|
| 5522 |
+
"micromark-util-character": "^2.0.0",
|
| 5523 |
+
"micromark-util-symbol": "^2.0.0",
|
| 5524 |
+
"micromark-util-types": "^2.0.0"
|
| 5525 |
+
}
|
| 5526 |
+
},
|
| 5527 |
+
"node_modules/micromark-factory-space": {
|
| 5528 |
+
"version": "2.0.1",
|
| 5529 |
+
"resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
|
| 5530 |
+
"integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
|
| 5531 |
+
"funding": [
|
| 5532 |
+
{
|
| 5533 |
+
"type": "GitHub Sponsors",
|
| 5534 |
+
"url": "https://github.com/sponsors/unifiedjs"
|
| 5535 |
+
},
|
| 5536 |
+
{
|
| 5537 |
+
"type": "OpenCollective",
|
| 5538 |
+
"url": "https://opencollective.com/unified"
|
| 5539 |
+
}
|
| 5540 |
+
],
|
| 5541 |
+
"license": "MIT",
|
| 5542 |
+
"dependencies": {
|
| 5543 |
+
"micromark-util-character": "^2.0.0",
|
| 5544 |
+
"micromark-util-types": "^2.0.0"
|
| 5545 |
+
}
|
| 5546 |
+
},
|
| 5547 |
+
"node_modules/micromark-factory-title": {
|
| 5548 |
+
"version": "2.0.1",
|
| 5549 |
+
"resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
|
| 5550 |
+
"integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
|
| 5551 |
+
"funding": [
|
| 5552 |
+
{
|
| 5553 |
+
"type": "GitHub Sponsors",
|
| 5554 |
+
"url": "https://github.com/sponsors/unifiedjs"
|
| 5555 |
+
},
|
| 5556 |
+
{
|
| 5557 |
+
"type": "OpenCollective",
|
| 5558 |
+
"url": "https://opencollective.com/unified"
|
| 5559 |
+
}
|
| 5560 |
+
],
|
| 5561 |
+
"license": "MIT",
|
| 5562 |
+
"dependencies": {
|
| 5563 |
+
"micromark-factory-space": "^2.0.0",
|
| 5564 |
+
"micromark-util-character": "^2.0.0",
|
| 5565 |
+
"micromark-util-symbol": "^2.0.0",
|
| 5566 |
+
"micromark-util-types": "^2.0.0"
|
| 5567 |
+
}
|
| 5568 |
+
},
|
| 5569 |
+
"node_modules/micromark-factory-whitespace": {
|
| 5570 |
+
"version": "2.0.1",
|
| 5571 |
+
"resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
|
| 5572 |
+
"integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
|
| 5573 |
+
"funding": [
|
| 5574 |
+
{
|
| 5575 |
+
"type": "GitHub Sponsors",
|
| 5576 |
+
"url": "https://github.com/sponsors/unifiedjs"
|
| 5577 |
+
},
|
| 5578 |
+
{
|
| 5579 |
+
"type": "OpenCollective",
|
| 5580 |
+
"url": "https://opencollective.com/unified"
|
| 5581 |
+
}
|
| 5582 |
+
],
|
| 5583 |
+
"license": "MIT",
|
| 5584 |
+
"dependencies": {
|
| 5585 |
+
"micromark-factory-space": "^2.0.0",
|
| 5586 |
+
"micromark-util-character": "^2.0.0",
|
| 5587 |
+
"micromark-util-symbol": "^2.0.0",
|
| 5588 |
+
"micromark-util-types": "^2.0.0"
|
| 5589 |
+
}
|
| 5590 |
+
},
|
| 5591 |
+
"node_modules/micromark-util-character": {
|
| 5592 |
+
"version": "2.1.1",
|
| 5593 |
+
"resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
|
| 5594 |
+
"integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
|
| 5595 |
+
"funding": [
|
| 5596 |
+
{
|
| 5597 |
+
"type": "GitHub Sponsors",
|
| 5598 |
+
"url": "https://github.com/sponsors/unifiedjs"
|
| 5599 |
+
},
|
| 5600 |
+
{
|
| 5601 |
+
"type": "OpenCollective",
|
| 5602 |
+
"url": "https://opencollective.com/unified"
|
| 5603 |
+
}
|
| 5604 |
+
],
|
| 5605 |
+
"license": "MIT",
|
| 5606 |
+
"dependencies": {
|
| 5607 |
+
"micromark-util-symbol": "^2.0.0",
|
| 5608 |
+
"micromark-util-types": "^2.0.0"
|
| 5609 |
+
}
|
| 5610 |
+
},
|
| 5611 |
+
"node_modules/micromark-util-chunked": {
|
| 5612 |
+
"version": "2.0.1",
|
| 5613 |
+
"resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
|
| 5614 |
+
"integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
|
| 5615 |
+
"funding": [
|
| 5616 |
+
{
|
| 5617 |
+
"type": "GitHub Sponsors",
|
| 5618 |
+
"url": "https://github.com/sponsors/unifiedjs"
|
| 5619 |
+
},
|
| 5620 |
+
{
|
| 5621 |
+
"type": "OpenCollective",
|
| 5622 |
+
"url": "https://opencollective.com/unified"
|
| 5623 |
+
}
|
| 5624 |
+
],
|
| 5625 |
+
"license": "MIT",
|
| 5626 |
+
"dependencies": {
|
| 5627 |
+
"micromark-util-symbol": "^2.0.0"
|
| 5628 |
+
}
|
| 5629 |
+
},
|
| 5630 |
+
"node_modules/micromark-util-classify-character": {
|
| 5631 |
+
"version": "2.0.1",
|
| 5632 |
+
"resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
|
| 5633 |
+
"integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
|
| 5634 |
+
"funding": [
|
| 5635 |
+
{
|
| 5636 |
+
"type": "GitHub Sponsors",
|
| 5637 |
+
"url": "https://github.com/sponsors/unifiedjs"
|
| 5638 |
+
},
|
| 5639 |
+
{
|
| 5640 |
+
"type": "OpenCollective",
|
| 5641 |
+
"url": "https://opencollective.com/unified"
|
| 5642 |
+
}
|
| 5643 |
+
],
|
| 5644 |
+
"license": "MIT",
|
| 5645 |
+
"dependencies": {
|
| 5646 |
+
"micromark-util-character": "^2.0.0",
|
| 5647 |
+
"micromark-util-symbol": "^2.0.0",
|
| 5648 |
+
"micromark-util-types": "^2.0.0"
|
| 5649 |
+
}
|
| 5650 |
+
},
|
| 5651 |
+
"node_modules/micromark-util-combine-extensions": {
|
| 5652 |
+
"version": "2.0.1",
|
| 5653 |
+
"resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
|
| 5654 |
+
"integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
|
| 5655 |
+
"funding": [
|
| 5656 |
+
{
|
| 5657 |
+
"type": "GitHub Sponsors",
|
| 5658 |
+
"url": "https://github.com/sponsors/unifiedjs"
|
| 5659 |
+
},
|
| 5660 |
+
{
|
| 5661 |
+
"type": "OpenCollective",
|
| 5662 |
+
"url": "https://opencollective.com/unified"
|
| 5663 |
+
}
|
| 5664 |
+
],
|
| 5665 |
+
"license": "MIT",
|
| 5666 |
+
"dependencies": {
|
| 5667 |
+
"micromark-util-chunked": "^2.0.0",
|
| 5668 |
+
"micromark-util-types": "^2.0.0"
|
| 5669 |
+
}
|
| 5670 |
+
},
|
| 5671 |
+
"node_modules/micromark-util-decode-numeric-character-reference": {
|
| 5672 |
+
"version": "2.0.2",
|
| 5673 |
+
"resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
|
| 5674 |
+
"integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
|
| 5675 |
+
"funding": [
|
| 5676 |
+
{
|
| 5677 |
+
"type": "GitHub Sponsors",
|
| 5678 |
+
"url": "https://github.com/sponsors/unifiedjs"
|
| 5679 |
+
},
|
| 5680 |
+
{
|
| 5681 |
+
"type": "OpenCollective",
|
| 5682 |
+
"url": "https://opencollective.com/unified"
|
| 5683 |
+
}
|
| 5684 |
+
],
|
| 5685 |
+
"license": "MIT",
|
| 5686 |
+
"dependencies": {
|
| 5687 |
+
"micromark-util-symbol": "^2.0.0"
|
| 5688 |
+
}
|
| 5689 |
+
},
|
| 5690 |
+
"node_modules/micromark-util-decode-string": {
|
| 5691 |
+
"version": "2.0.1",
|
| 5692 |
+
"resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
|
| 5693 |
+
"integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
|
| 5694 |
+
"funding": [
|
| 5695 |
+
{
|
| 5696 |
+
"type": "GitHub Sponsors",
|
| 5697 |
+
"url": "https://github.com/sponsors/unifiedjs"
|
| 5698 |
+
},
|
| 5699 |
+
{
|
| 5700 |
+
"type": "OpenCollective",
|
| 5701 |
+
"url": "https://opencollective.com/unified"
|
| 5702 |
+
}
|
| 5703 |
+
],
|
| 5704 |
+
"license": "MIT",
|
| 5705 |
+
"dependencies": {
|
| 5706 |
+
"decode-named-character-reference": "^1.0.0",
|
| 5707 |
+
"micromark-util-character": "^2.0.0",
|
| 5708 |
+
"micromark-util-decode-numeric-character-reference": "^2.0.0",
|
| 5709 |
+
"micromark-util-symbol": "^2.0.0"
|
| 5710 |
+
}
|
| 5711 |
+
},
|
| 5712 |
+
"node_modules/micromark-util-encode": {
|
| 5713 |
+
"version": "2.0.1",
|
| 5714 |
+
"resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
|
| 5715 |
+
"integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
|
| 5716 |
+
"funding": [
|
| 5717 |
+
{
|
| 5718 |
+
"type": "GitHub Sponsors",
|
| 5719 |
+
"url": "https://github.com/sponsors/unifiedjs"
|
| 5720 |
+
},
|
| 5721 |
+
{
|
| 5722 |
+
"type": "OpenCollective",
|
| 5723 |
+
"url": "https://opencollective.com/unified"
|
| 5724 |
+
}
|
| 5725 |
+
],
|
| 5726 |
+
"license": "MIT"
|
| 5727 |
+
},
|
| 5728 |
+
"node_modules/micromark-util-html-tag-name": {
|
| 5729 |
+
"version": "2.0.1",
|
| 5730 |
+
"resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
|
| 5731 |
+
"integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
|
| 5732 |
+
"funding": [
|
| 5733 |
+
{
|
| 5734 |
+
"type": "GitHub Sponsors",
|
| 5735 |
+
"url": "https://github.com/sponsors/unifiedjs"
|
| 5736 |
+
},
|
| 5737 |
+
{
|
| 5738 |
+
"type": "OpenCollective",
|
| 5739 |
+
"url": "https://opencollective.com/unified"
|
| 5740 |
+
}
|
| 5741 |
+
],
|
| 5742 |
+
"license": "MIT"
|
| 5743 |
+
},
|
| 5744 |
+
"node_modules/micromark-util-normalize-identifier": {
|
| 5745 |
+
"version": "2.0.1",
|
| 5746 |
+
"resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
|
| 5747 |
+
"integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
|
| 5748 |
+
"funding": [
|
| 5749 |
+
{
|
| 5750 |
+
"type": "GitHub Sponsors",
|
| 5751 |
+
"url": "https://github.com/sponsors/unifiedjs"
|
| 5752 |
+
},
|
| 5753 |
+
{
|
| 5754 |
+
"type": "OpenCollective",
|
| 5755 |
+
"url": "https://opencollective.com/unified"
|
| 5756 |
+
}
|
| 5757 |
+
],
|
| 5758 |
+
"license": "MIT",
|
| 5759 |
+
"dependencies": {
|
| 5760 |
+
"micromark-util-symbol": "^2.0.0"
|
| 5761 |
+
}
|
| 5762 |
+
},
|
| 5763 |
+
"node_modules/micromark-util-resolve-all": {
|
| 5764 |
+
"version": "2.0.1",
|
| 5765 |
+
"resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
|
| 5766 |
+
"integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
|
| 5767 |
+
"funding": [
|
| 5768 |
+
{
|
| 5769 |
+
"type": "GitHub Sponsors",
|
| 5770 |
+
"url": "https://github.com/sponsors/unifiedjs"
|
| 5771 |
+
},
|
| 5772 |
+
{
|
| 5773 |
+
"type": "OpenCollective",
|
| 5774 |
+
"url": "https://opencollective.com/unified"
|
| 5775 |
+
}
|
| 5776 |
+
],
|
| 5777 |
+
"license": "MIT",
|
| 5778 |
+
"dependencies": {
|
| 5779 |
+
"micromark-util-types": "^2.0.0"
|
| 5780 |
+
}
|
| 5781 |
+
},
|
| 5782 |
+
"node_modules/micromark-util-sanitize-uri": {
|
| 5783 |
+
"version": "2.0.1",
|
| 5784 |
+
"resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
|
| 5785 |
+
"integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
|
| 5786 |
+
"funding": [
|
| 5787 |
+
{
|
| 5788 |
+
"type": "GitHub Sponsors",
|
| 5789 |
+
"url": "https://github.com/sponsors/unifiedjs"
|
| 5790 |
+
},
|
| 5791 |
+
{
|
| 5792 |
+
"type": "OpenCollective",
|
| 5793 |
+
"url": "https://opencollective.com/unified"
|
| 5794 |
+
}
|
| 5795 |
+
],
|
| 5796 |
+
"license": "MIT",
|
| 5797 |
+
"dependencies": {
|
| 5798 |
+
"micromark-util-character": "^2.0.0",
|
| 5799 |
+
"micromark-util-encode": "^2.0.0",
|
| 5800 |
+
"micromark-util-symbol": "^2.0.0"
|
| 5801 |
+
}
|
| 5802 |
+
},
|
| 5803 |
+
"node_modules/micromark-util-subtokenize": {
|
| 5804 |
+
"version": "2.1.0",
|
| 5805 |
+
"resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
|
| 5806 |
+
"integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
|
| 5807 |
+
"funding": [
|
| 5808 |
+
{
|
| 5809 |
+
"type": "GitHub Sponsors",
|
| 5810 |
+
"url": "https://github.com/sponsors/unifiedjs"
|
| 5811 |
+
},
|
| 5812 |
+
{
|
| 5813 |
+
"type": "OpenCollective",
|
| 5814 |
+
"url": "https://opencollective.com/unified"
|
| 5815 |
+
}
|
| 5816 |
+
],
|
| 5817 |
+
"license": "MIT",
|
| 5818 |
+
"dependencies": {
|
| 5819 |
+
"devlop": "^1.0.0",
|
| 5820 |
+
"micromark-util-chunked": "^2.0.0",
|
| 5821 |
+
"micromark-util-symbol": "^2.0.0",
|
| 5822 |
+
"micromark-util-types": "^2.0.0"
|
| 5823 |
+
}
|
| 5824 |
+
},
|
| 5825 |
+
"node_modules/micromark-util-symbol": {
|
| 5826 |
+
"version": "2.0.1",
|
| 5827 |
+
"resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
|
| 5828 |
+
"integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
|
| 5829 |
+
"funding": [
|
| 5830 |
+
{
|
| 5831 |
+
"type": "GitHub Sponsors",
|
| 5832 |
+
"url": "https://github.com/sponsors/unifiedjs"
|
| 5833 |
+
},
|
| 5834 |
+
{
|
| 5835 |
+
"type": "OpenCollective",
|
| 5836 |
+
"url": "https://opencollective.com/unified"
|
| 5837 |
+
}
|
| 5838 |
+
],
|
| 5839 |
+
"license": "MIT"
|
| 5840 |
+
},
|
| 5841 |
+
"node_modules/micromark-util-types": {
|
| 5842 |
+
"version": "2.0.2",
|
| 5843 |
+
"resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
|
| 5844 |
+
"integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
|
| 5845 |
+
"funding": [
|
| 5846 |
+
{
|
| 5847 |
+
"type": "GitHub Sponsors",
|
| 5848 |
+
"url": "https://github.com/sponsors/unifiedjs"
|
| 5849 |
+
},
|
| 5850 |
+
{
|
| 5851 |
+
"type": "OpenCollective",
|
| 5852 |
+
"url": "https://opencollective.com/unified"
|
| 5853 |
+
}
|
| 5854 |
+
],
|
| 5855 |
+
"license": "MIT"
|
| 5856 |
+
},
|
| 5857 |
+
"node_modules/minipass": {
|
| 5858 |
+
"version": "7.1.3",
|
| 5859 |
+
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
| 5860 |
+
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
| 5861 |
+
"dev": true,
|
| 5862 |
+
"license": "BlueOak-1.0.0",
|
| 5863 |
+
"engines": {
|
| 5864 |
+
"node": ">=16 || 14 >=14.17"
|
| 5865 |
+
}
|
| 5866 |
+
},
|
| 5867 |
+
"node_modules/minizlib": {
|
| 5868 |
+
"version": "3.1.0",
|
| 5869 |
+
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
|
| 5870 |
+
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
|
| 5871 |
+
"dev": true,
|
| 5872 |
+
"license": "MIT",
|
| 5873 |
+
"dependencies": {
|
| 5874 |
+
"minipass": "^7.1.2"
|
| 5875 |
+
},
|
| 5876 |
+
"engines": {
|
| 5877 |
+
"node": ">= 18"
|
| 5878 |
+
}
|
| 5879 |
+
},
|
| 5880 |
+
"node_modules/motion": {
|
| 5881 |
+
"version": "12.23.24",
|
| 5882 |
+
"resolved": "https://registry.npmjs.org/motion/-/motion-12.23.24.tgz",
|
| 5883 |
"integrity": "sha512-Rc5E7oe2YZ72N//S3QXGzbnXgqNrTESv8KKxABR20q2FLch9gHLo0JLyYo2hZ238bZ9Gx6cWhj9VO0IgwbMjCw==",
|
| 5884 |
"license": "MIT",
|
| 5885 |
"dependencies": {
|
|
|
|
| 5981 |
"node": ">=6"
|
| 5982 |
}
|
| 5983 |
},
|
| 5984 |
+
"node_modules/parse-entities": {
|
| 5985 |
+
"version": "4.0.2",
|
| 5986 |
+
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
|
| 5987 |
+
"integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
|
| 5988 |
+
"license": "MIT",
|
| 5989 |
+
"dependencies": {
|
| 5990 |
+
"@types/unist": "^2.0.0",
|
| 5991 |
+
"character-entities-legacy": "^3.0.0",
|
| 5992 |
+
"character-reference-invalid": "^2.0.0",
|
| 5993 |
+
"decode-named-character-reference": "^1.0.0",
|
| 5994 |
+
"is-alphanumerical": "^2.0.0",
|
| 5995 |
+
"is-decimal": "^2.0.0",
|
| 5996 |
+
"is-hexadecimal": "^2.0.0"
|
| 5997 |
+
},
|
| 5998 |
+
"funding": {
|
| 5999 |
+
"type": "github",
|
| 6000 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 6001 |
+
}
|
| 6002 |
+
},
|
| 6003 |
+
"node_modules/parse-entities/node_modules/@types/unist": {
|
| 6004 |
+
"version": "2.0.11",
|
| 6005 |
+
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
|
| 6006 |
+
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
|
| 6007 |
+
"license": "MIT"
|
| 6008 |
+
},
|
| 6009 |
"node_modules/parse-json": {
|
| 6010 |
"version": "5.2.0",
|
| 6011 |
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
|
|
|
| 6024 |
"url": "https://github.com/sponsors/sindresorhus"
|
| 6025 |
}
|
| 6026 |
},
|
| 6027 |
+
"node_modules/parse5": {
|
| 6028 |
+
"version": "7.3.0",
|
| 6029 |
+
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
| 6030 |
+
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
| 6031 |
+
"license": "MIT",
|
| 6032 |
+
"dependencies": {
|
| 6033 |
+
"entities": "^6.0.0"
|
| 6034 |
+
},
|
| 6035 |
+
"funding": {
|
| 6036 |
+
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
| 6037 |
+
}
|
| 6038 |
+
},
|
| 6039 |
"node_modules/path-parse": {
|
| 6040 |
"version": "1.0.7",
|
| 6041 |
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
|
|
|
| 6116 |
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
| 6117 |
"license": "MIT"
|
| 6118 |
},
|
| 6119 |
+
"node_modules/property-information": {
|
| 6120 |
+
"version": "7.1.0",
|
| 6121 |
+
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
|
| 6122 |
+
"integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
|
| 6123 |
+
"license": "MIT",
|
| 6124 |
+
"funding": {
|
| 6125 |
+
"type": "github",
|
| 6126 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 6127 |
+
}
|
| 6128 |
+
},
|
| 6129 |
"node_modules/react": {
|
| 6130 |
"version": "18.3.1",
|
| 6131 |
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
|
|
|
| 6234 |
"integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==",
|
| 6235 |
"license": "MIT"
|
| 6236 |
},
|
| 6237 |
+
"node_modules/react-markdown": {
|
| 6238 |
+
"version": "10.1.0",
|
| 6239 |
+
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
|
| 6240 |
+
"integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
|
| 6241 |
+
"license": "MIT",
|
| 6242 |
+
"dependencies": {
|
| 6243 |
+
"@types/hast": "^3.0.0",
|
| 6244 |
+
"@types/mdast": "^4.0.0",
|
| 6245 |
+
"devlop": "^1.0.0",
|
| 6246 |
+
"hast-util-to-jsx-runtime": "^2.0.0",
|
| 6247 |
+
"html-url-attributes": "^3.0.0",
|
| 6248 |
+
"mdast-util-to-hast": "^13.0.0",
|
| 6249 |
+
"remark-parse": "^11.0.0",
|
| 6250 |
+
"remark-rehype": "^11.0.0",
|
| 6251 |
+
"unified": "^11.0.0",
|
| 6252 |
+
"unist-util-visit": "^5.0.0",
|
| 6253 |
+
"vfile": "^6.0.0"
|
| 6254 |
+
},
|
| 6255 |
+
"funding": {
|
| 6256 |
+
"type": "opencollective",
|
| 6257 |
+
"url": "https://opencollective.com/unified"
|
| 6258 |
+
},
|
| 6259 |
+
"peerDependencies": {
|
| 6260 |
+
"@types/react": ">=18",
|
| 6261 |
+
"react": ">=18"
|
| 6262 |
+
}
|
| 6263 |
+
},
|
| 6264 |
"node_modules/react-popper": {
|
| 6265 |
"version": "2.3.0",
|
| 6266 |
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz",
|
|
|
|
| 6487 |
"@babel/runtime": "^7.9.2"
|
| 6488 |
}
|
| 6489 |
},
|
| 6490 |
+
"node_modules/rehype-katex": {
|
| 6491 |
+
"version": "7.0.1",
|
| 6492 |
+
"resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz",
|
| 6493 |
+
"integrity": "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==",
|
| 6494 |
+
"license": "MIT",
|
| 6495 |
+
"dependencies": {
|
| 6496 |
+
"@types/hast": "^3.0.0",
|
| 6497 |
+
"@types/katex": "^0.16.0",
|
| 6498 |
+
"hast-util-from-html-isomorphic": "^2.0.0",
|
| 6499 |
+
"hast-util-to-text": "^4.0.0",
|
| 6500 |
+
"katex": "^0.16.0",
|
| 6501 |
+
"unist-util-visit-parents": "^6.0.0",
|
| 6502 |
+
"vfile": "^6.0.0"
|
| 6503 |
+
},
|
| 6504 |
+
"funding": {
|
| 6505 |
+
"type": "opencollective",
|
| 6506 |
+
"url": "https://opencollective.com/unified"
|
| 6507 |
+
}
|
| 6508 |
+
},
|
| 6509 |
+
"node_modules/remark-gfm": {
|
| 6510 |
+
"version": "4.0.1",
|
| 6511 |
+
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
|
| 6512 |
+
"integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
|
| 6513 |
+
"license": "MIT",
|
| 6514 |
+
"dependencies": {
|
| 6515 |
+
"@types/mdast": "^4.0.0",
|
| 6516 |
+
"mdast-util-gfm": "^3.0.0",
|
| 6517 |
+
"micromark-extension-gfm": "^3.0.0",
|
| 6518 |
+
"remark-parse": "^11.0.0",
|
| 6519 |
+
"remark-stringify": "^11.0.0",
|
| 6520 |
+
"unified": "^11.0.0"
|
| 6521 |
+
},
|
| 6522 |
+
"funding": {
|
| 6523 |
+
"type": "opencollective",
|
| 6524 |
+
"url": "https://opencollective.com/unified"
|
| 6525 |
+
}
|
| 6526 |
+
},
|
| 6527 |
+
"node_modules/remark-math": {
|
| 6528 |
+
"version": "6.0.0",
|
| 6529 |
+
"resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz",
|
| 6530 |
+
"integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==",
|
| 6531 |
+
"license": "MIT",
|
| 6532 |
+
"dependencies": {
|
| 6533 |
+
"@types/mdast": "^4.0.0",
|
| 6534 |
+
"mdast-util-math": "^3.0.0",
|
| 6535 |
+
"micromark-extension-math": "^3.0.0",
|
| 6536 |
+
"unified": "^11.0.0"
|
| 6537 |
+
},
|
| 6538 |
+
"funding": {
|
| 6539 |
+
"type": "opencollective",
|
| 6540 |
+
"url": "https://opencollective.com/unified"
|
| 6541 |
+
}
|
| 6542 |
+
},
|
| 6543 |
+
"node_modules/remark-parse": {
|
| 6544 |
+
"version": "11.0.0",
|
| 6545 |
+
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
|
| 6546 |
+
"integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
|
| 6547 |
+
"license": "MIT",
|
| 6548 |
+
"dependencies": {
|
| 6549 |
+
"@types/mdast": "^4.0.0",
|
| 6550 |
+
"mdast-util-from-markdown": "^2.0.0",
|
| 6551 |
+
"micromark-util-types": "^2.0.0",
|
| 6552 |
+
"unified": "^11.0.0"
|
| 6553 |
+
},
|
| 6554 |
+
"funding": {
|
| 6555 |
+
"type": "opencollective",
|
| 6556 |
+
"url": "https://opencollective.com/unified"
|
| 6557 |
+
}
|
| 6558 |
+
},
|
| 6559 |
+
"node_modules/remark-rehype": {
|
| 6560 |
+
"version": "11.1.2",
|
| 6561 |
+
"resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
|
| 6562 |
+
"integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
|
| 6563 |
+
"license": "MIT",
|
| 6564 |
+
"dependencies": {
|
| 6565 |
+
"@types/hast": "^3.0.0",
|
| 6566 |
+
"@types/mdast": "^4.0.0",
|
| 6567 |
+
"mdast-util-to-hast": "^13.0.0",
|
| 6568 |
+
"unified": "^11.0.0",
|
| 6569 |
+
"vfile": "^6.0.0"
|
| 6570 |
+
},
|
| 6571 |
+
"funding": {
|
| 6572 |
+
"type": "opencollective",
|
| 6573 |
+
"url": "https://opencollective.com/unified"
|
| 6574 |
+
}
|
| 6575 |
+
},
|
| 6576 |
+
"node_modules/remark-stringify": {
|
| 6577 |
+
"version": "11.0.0",
|
| 6578 |
+
"resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
|
| 6579 |
+
"integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
|
| 6580 |
+
"license": "MIT",
|
| 6581 |
+
"dependencies": {
|
| 6582 |
+
"@types/mdast": "^4.0.0",
|
| 6583 |
+
"mdast-util-to-markdown": "^2.0.0",
|
| 6584 |
+
"unified": "^11.0.0"
|
| 6585 |
+
},
|
| 6586 |
+
"funding": {
|
| 6587 |
+
"type": "opencollective",
|
| 6588 |
+
"url": "https://opencollective.com/unified"
|
| 6589 |
+
}
|
| 6590 |
+
},
|
| 6591 |
"node_modules/resize-observer-polyfill": {
|
| 6592 |
"version": "1.5.1",
|
| 6593 |
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
|
|
|
| 6723 |
"node": ">=0.10.0"
|
| 6724 |
}
|
| 6725 |
},
|
| 6726 |
+
"node_modules/space-separated-tokens": {
|
| 6727 |
+
"version": "2.0.2",
|
| 6728 |
+
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
|
| 6729 |
+
"integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
|
| 6730 |
+
"license": "MIT",
|
| 6731 |
+
"funding": {
|
| 6732 |
+
"type": "github",
|
| 6733 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 6734 |
+
}
|
| 6735 |
+
},
|
| 6736 |
"node_modules/string-convert": {
|
| 6737 |
"version": "0.2.1",
|
| 6738 |
"resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
|
| 6739 |
"integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==",
|
| 6740 |
"license": "MIT"
|
| 6741 |
},
|
| 6742 |
+
"node_modules/stringify-entities": {
|
| 6743 |
+
"version": "4.0.4",
|
| 6744 |
+
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
|
| 6745 |
+
"integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
|
| 6746 |
+
"license": "MIT",
|
| 6747 |
+
"dependencies": {
|
| 6748 |
+
"character-entities-html4": "^2.0.0",
|
| 6749 |
+
"character-entities-legacy": "^3.0.0"
|
| 6750 |
+
},
|
| 6751 |
+
"funding": {
|
| 6752 |
+
"type": "github",
|
| 6753 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 6754 |
+
}
|
| 6755 |
+
},
|
| 6756 |
+
"node_modules/style-to-js": {
|
| 6757 |
+
"version": "1.1.21",
|
| 6758 |
+
"resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
|
| 6759 |
+
"integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
|
| 6760 |
+
"license": "MIT",
|
| 6761 |
+
"dependencies": {
|
| 6762 |
+
"style-to-object": "1.0.14"
|
| 6763 |
+
}
|
| 6764 |
+
},
|
| 6765 |
+
"node_modules/style-to-object": {
|
| 6766 |
+
"version": "1.0.14",
|
| 6767 |
+
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
|
| 6768 |
+
"integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
|
| 6769 |
+
"license": "MIT",
|
| 6770 |
+
"dependencies": {
|
| 6771 |
+
"inline-style-parser": "0.2.7"
|
| 6772 |
+
}
|
| 6773 |
+
},
|
| 6774 |
"node_modules/stylis": {
|
| 6775 |
"version": "4.2.0",
|
| 6776 |
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
|
|
|
|
| 6870 |
"url": "https://github.com/sponsors/SuperchupuDev"
|
| 6871 |
}
|
| 6872 |
},
|
| 6873 |
+
"node_modules/trim-lines": {
|
| 6874 |
+
"version": "3.0.1",
|
| 6875 |
+
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
|
| 6876 |
+
"integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
|
| 6877 |
+
"license": "MIT",
|
| 6878 |
+
"funding": {
|
| 6879 |
+
"type": "github",
|
| 6880 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 6881 |
+
}
|
| 6882 |
+
},
|
| 6883 |
+
"node_modules/trough": {
|
| 6884 |
+
"version": "2.2.0",
|
| 6885 |
+
"resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
|
| 6886 |
+
"integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
|
| 6887 |
+
"license": "MIT",
|
| 6888 |
+
"funding": {
|
| 6889 |
+
"type": "github",
|
| 6890 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 6891 |
+
}
|
| 6892 |
+
},
|
| 6893 |
"node_modules/tslib": {
|
| 6894 |
"version": "2.8.1",
|
| 6895 |
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
|
|
|
| 6905 |
"url": "https://github.com/sponsors/Wombosvideo"
|
| 6906 |
}
|
| 6907 |
},
|
| 6908 |
+
"node_modules/unified": {
|
| 6909 |
+
"version": "11.0.5",
|
| 6910 |
+
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
|
| 6911 |
+
"integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
|
| 6912 |
+
"license": "MIT",
|
| 6913 |
+
"dependencies": {
|
| 6914 |
+
"@types/unist": "^3.0.0",
|
| 6915 |
+
"bail": "^2.0.0",
|
| 6916 |
+
"devlop": "^1.0.0",
|
| 6917 |
+
"extend": "^3.0.0",
|
| 6918 |
+
"is-plain-obj": "^4.0.0",
|
| 6919 |
+
"trough": "^2.0.0",
|
| 6920 |
+
"vfile": "^6.0.0"
|
| 6921 |
+
},
|
| 6922 |
+
"funding": {
|
| 6923 |
+
"type": "opencollective",
|
| 6924 |
+
"url": "https://opencollective.com/unified"
|
| 6925 |
+
}
|
| 6926 |
+
},
|
| 6927 |
+
"node_modules/unist-util-find-after": {
|
| 6928 |
+
"version": "5.0.0",
|
| 6929 |
+
"resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz",
|
| 6930 |
+
"integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==",
|
| 6931 |
+
"license": "MIT",
|
| 6932 |
+
"dependencies": {
|
| 6933 |
+
"@types/unist": "^3.0.0",
|
| 6934 |
+
"unist-util-is": "^6.0.0"
|
| 6935 |
+
},
|
| 6936 |
+
"funding": {
|
| 6937 |
+
"type": "opencollective",
|
| 6938 |
+
"url": "https://opencollective.com/unified"
|
| 6939 |
+
}
|
| 6940 |
+
},
|
| 6941 |
+
"node_modules/unist-util-is": {
|
| 6942 |
+
"version": "6.0.1",
|
| 6943 |
+
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
|
| 6944 |
+
"integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
|
| 6945 |
+
"license": "MIT",
|
| 6946 |
+
"dependencies": {
|
| 6947 |
+
"@types/unist": "^3.0.0"
|
| 6948 |
+
},
|
| 6949 |
+
"funding": {
|
| 6950 |
+
"type": "opencollective",
|
| 6951 |
+
"url": "https://opencollective.com/unified"
|
| 6952 |
+
}
|
| 6953 |
+
},
|
| 6954 |
+
"node_modules/unist-util-position": {
|
| 6955 |
+
"version": "5.0.0",
|
| 6956 |
+
"resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
|
| 6957 |
+
"integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
|
| 6958 |
+
"license": "MIT",
|
| 6959 |
+
"dependencies": {
|
| 6960 |
+
"@types/unist": "^3.0.0"
|
| 6961 |
+
},
|
| 6962 |
+
"funding": {
|
| 6963 |
+
"type": "opencollective",
|
| 6964 |
+
"url": "https://opencollective.com/unified"
|
| 6965 |
+
}
|
| 6966 |
+
},
|
| 6967 |
+
"node_modules/unist-util-remove-position": {
|
| 6968 |
+
"version": "5.0.0",
|
| 6969 |
+
"resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz",
|
| 6970 |
+
"integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==",
|
| 6971 |
+
"license": "MIT",
|
| 6972 |
+
"dependencies": {
|
| 6973 |
+
"@types/unist": "^3.0.0",
|
| 6974 |
+
"unist-util-visit": "^5.0.0"
|
| 6975 |
+
},
|
| 6976 |
+
"funding": {
|
| 6977 |
+
"type": "opencollective",
|
| 6978 |
+
"url": "https://opencollective.com/unified"
|
| 6979 |
+
}
|
| 6980 |
+
},
|
| 6981 |
+
"node_modules/unist-util-stringify-position": {
|
| 6982 |
+
"version": "4.0.0",
|
| 6983 |
+
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
|
| 6984 |
+
"integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
|
| 6985 |
+
"license": "MIT",
|
| 6986 |
+
"dependencies": {
|
| 6987 |
+
"@types/unist": "^3.0.0"
|
| 6988 |
+
},
|
| 6989 |
+
"funding": {
|
| 6990 |
+
"type": "opencollective",
|
| 6991 |
+
"url": "https://opencollective.com/unified"
|
| 6992 |
+
}
|
| 6993 |
+
},
|
| 6994 |
+
"node_modules/unist-util-visit": {
|
| 6995 |
+
"version": "5.1.0",
|
| 6996 |
+
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
|
| 6997 |
+
"integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
|
| 6998 |
+
"license": "MIT",
|
| 6999 |
+
"dependencies": {
|
| 7000 |
+
"@types/unist": "^3.0.0",
|
| 7001 |
+
"unist-util-is": "^6.0.0",
|
| 7002 |
+
"unist-util-visit-parents": "^6.0.0"
|
| 7003 |
+
},
|
| 7004 |
+
"funding": {
|
| 7005 |
+
"type": "opencollective",
|
| 7006 |
+
"url": "https://opencollective.com/unified"
|
| 7007 |
+
}
|
| 7008 |
+
},
|
| 7009 |
+
"node_modules/unist-util-visit-parents": {
|
| 7010 |
+
"version": "6.0.2",
|
| 7011 |
+
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
|
| 7012 |
+
"integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
|
| 7013 |
+
"license": "MIT",
|
| 7014 |
+
"dependencies": {
|
| 7015 |
+
"@types/unist": "^3.0.0",
|
| 7016 |
+
"unist-util-is": "^6.0.0"
|
| 7017 |
+
},
|
| 7018 |
+
"funding": {
|
| 7019 |
+
"type": "opencollective",
|
| 7020 |
+
"url": "https://opencollective.com/unified"
|
| 7021 |
+
}
|
| 7022 |
+
},
|
| 7023 |
"node_modules/update-browserslist-db": {
|
| 7024 |
"version": "1.2.3",
|
| 7025 |
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
|
|
|
| 7107 |
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
|
| 7108 |
}
|
| 7109 |
},
|
| 7110 |
+
"node_modules/vfile": {
|
| 7111 |
+
"version": "6.0.3",
|
| 7112 |
+
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
|
| 7113 |
+
"integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
|
| 7114 |
+
"license": "MIT",
|
| 7115 |
+
"dependencies": {
|
| 7116 |
+
"@types/unist": "^3.0.0",
|
| 7117 |
+
"vfile-message": "^4.0.0"
|
| 7118 |
+
},
|
| 7119 |
+
"funding": {
|
| 7120 |
+
"type": "opencollective",
|
| 7121 |
+
"url": "https://opencollective.com/unified"
|
| 7122 |
+
}
|
| 7123 |
+
},
|
| 7124 |
+
"node_modules/vfile-location": {
|
| 7125 |
+
"version": "5.0.3",
|
| 7126 |
+
"resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz",
|
| 7127 |
+
"integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==",
|
| 7128 |
+
"license": "MIT",
|
| 7129 |
+
"dependencies": {
|
| 7130 |
+
"@types/unist": "^3.0.0",
|
| 7131 |
+
"vfile": "^6.0.0"
|
| 7132 |
+
},
|
| 7133 |
+
"funding": {
|
| 7134 |
+
"type": "opencollective",
|
| 7135 |
+
"url": "https://opencollective.com/unified"
|
| 7136 |
+
}
|
| 7137 |
+
},
|
| 7138 |
+
"node_modules/vfile-message": {
|
| 7139 |
+
"version": "4.0.3",
|
| 7140 |
+
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
|
| 7141 |
+
"integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
|
| 7142 |
+
"license": "MIT",
|
| 7143 |
+
"dependencies": {
|
| 7144 |
+
"@types/unist": "^3.0.0",
|
| 7145 |
+
"unist-util-stringify-position": "^4.0.0"
|
| 7146 |
+
},
|
| 7147 |
+
"funding": {
|
| 7148 |
+
"type": "opencollective",
|
| 7149 |
+
"url": "https://opencollective.com/unified"
|
| 7150 |
+
}
|
| 7151 |
+
},
|
| 7152 |
"node_modules/victory-vendor": {
|
| 7153 |
"version": "36.9.2",
|
| 7154 |
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
|
|
|
|
| 7255 |
"loose-envify": "^1.0.0"
|
| 7256 |
}
|
| 7257 |
},
|
| 7258 |
+
"node_modules/web-namespaces": {
|
| 7259 |
+
"version": "2.0.1",
|
| 7260 |
+
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
|
| 7261 |
+
"integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==",
|
| 7262 |
+
"license": "MIT",
|
| 7263 |
+
"funding": {
|
| 7264 |
+
"type": "github",
|
| 7265 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 7266 |
+
}
|
| 7267 |
+
},
|
| 7268 |
"node_modules/yallist": {
|
| 7269 |
"version": "3.1.1",
|
| 7270 |
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
|
|
|
| 7289 |
"funding": {
|
| 7290 |
"url": "https://github.com/sponsors/eemeli"
|
| 7291 |
}
|
| 7292 |
+
},
|
| 7293 |
+
"node_modules/zwitch": {
|
| 7294 |
+
"version": "2.0.4",
|
| 7295 |
+
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
| 7296 |
+
"integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
|
| 7297 |
+
"license": "MIT",
|
| 7298 |
+
"funding": {
|
| 7299 |
+
"type": "github",
|
| 7300 |
+
"url": "https://github.com/sponsors/wooorm"
|
| 7301 |
+
}
|
| 7302 |
}
|
| 7303 |
}
|
| 7304 |
}
|
public/maintiva-logo.jpg
ADDED
|
public/sounds/01_Baik_Saya_sedang_memproses_pertanyaanmu.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cb6216abe80579ac0815d0b5afe2a832002fa86fc892f90e18a92054d8a94560
|
| 3 |
+
size 930838
|
public/sounds/02_Oke_mohon_ditunggu_saya_sedang_siapkan_P.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:128880cb46eb3c232e466ad6da94c08073597dd2b8916f1554d3ed933b90511c
|
| 3 |
+
size 1057558
|
public/sounds/03_Sip_saya_terima_Sedang_saya_proses_Pesan.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7035343867739852b9bcc5f2b3310cabf3f0b8341baa715cbe0f15c20ca4be52
|
| 3 |
+
size 811798
|
server.js
CHANGED
|
@@ -26,7 +26,7 @@ const MIME = {
|
|
| 26 |
};
|
| 27 |
|
| 28 |
console.log(`Starting server on port ${PORT}`);
|
| 29 |
-
console.log(`Backend URL: ${BACKEND_URL || "(not set)"}`);
|
| 30 |
|
| 31 |
const server = http.createServer((req, res) => {
|
| 32 |
const parsed = url.parse(req.url);
|
|
@@ -77,6 +77,18 @@ const server = http.createServer((req, res) => {
|
|
| 77 |
return;
|
| 78 |
}
|
| 79 |
const ext = path.extname(filePath).toLowerCase();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
res.writeHead(200, { "Content-Type": MIME[ext] || "application/octet-stream" });
|
| 81 |
res.end(data);
|
| 82 |
});
|
|
|
|
| 26 |
};
|
| 27 |
|
| 28 |
console.log(`Starting server on port ${PORT}`);
|
| 29 |
+
// console.log(`Backend URL: ${BACKEND_URL || "(not set)"}`);
|
| 30 |
|
| 31 |
const server = http.createServer((req, res) => {
|
| 32 |
const parsed = url.parse(req.url);
|
|
|
|
| 77 |
return;
|
| 78 |
}
|
| 79 |
const ext = path.extname(filePath).toLowerCase();
|
| 80 |
+
|
| 81 |
+
if (ext === ".html") {
|
| 82 |
+
const config = { VOICE_API_URL: process.env.VITE_API_BASE_VOICE_URL || "" };
|
| 83 |
+
const html = data.toString().replace(
|
| 84 |
+
"</head>",
|
| 85 |
+
`<script>window.__APP_CONFIG__=${JSON.stringify(config)};</script></head>`
|
| 86 |
+
);
|
| 87 |
+
res.writeHead(200, { "Content-Type": "text/html" });
|
| 88 |
+
res.end(html);
|
| 89 |
+
return;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
res.writeHead(200, { "Content-Type": MIME[ext] || "application/octet-stream" });
|
| 93 |
res.end(data);
|
| 94 |
});
|
src/app/App.tsx
CHANGED
|
@@ -1,6 +1,12 @@
|
|
| 1 |
import { RouterProvider } from "react-router";
|
| 2 |
import { router } from "./routes";
|
|
|
|
| 3 |
|
| 4 |
export default function App() {
|
| 5 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
}
|
|
|
|
| 1 |
import { RouterProvider } from "react-router";
|
| 2 |
import { router } from "./routes";
|
| 3 |
+
import { Toaster } from "./components/ui/sonner";
|
| 4 |
|
| 5 |
export default function App() {
|
| 6 |
+
return (
|
| 7 |
+
<>
|
| 8 |
+
<RouterProvider router={router} />
|
| 9 |
+
<Toaster richColors position="top-right" />
|
| 10 |
+
</>
|
| 11 |
+
);
|
| 12 |
}
|
src/app/components/KnowledgeManagement.tsx
CHANGED
|
@@ -7,14 +7,27 @@ import {
|
|
| 7 |
Loader2,
|
| 8 |
Database,
|
| 9 |
X,
|
|
|
|
|
|
|
| 10 |
} from "lucide-react";
|
|
|
|
| 11 |
import {
|
| 12 |
getDocuments,
|
| 13 |
uploadDocument,
|
| 14 |
processDocument,
|
| 15 |
deleteDocument,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
type ApiDocument,
|
| 17 |
type DocumentStatus,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
} from "../../services/api";
|
| 19 |
|
| 20 |
interface KnowledgeManagementProps {
|
|
@@ -22,6 +35,17 @@ interface KnowledgeManagementProps {
|
|
| 22 |
onClose: () => void;
|
| 23 |
}
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
const getUserId = (): string | null => {
|
| 26 |
const stored = localStorage.getItem("chatbot_user");
|
| 27 |
if (!stored) return null;
|
|
@@ -32,6 +56,8 @@ export default function KnowledgeManagement({
|
|
| 32 |
open,
|
| 33 |
onClose,
|
| 34 |
}: KnowledgeManagementProps) {
|
|
|
|
|
|
|
| 35 |
const [documents, setDocuments] = useState<ApiDocument[]>([]);
|
| 36 |
const [loadingDocs, setLoadingDocs] = useState(false);
|
| 37 |
const [docsError, setDocsError] = useState<string | null>(null);
|
|
@@ -40,13 +66,68 @@ export default function KnowledgeManagement({
|
|
| 40 |
const [processing, setProcessing] = useState<string | null>(null);
|
| 41 |
const [deleting, setDeleting] = useState<string | null>(null);
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
useEffect(() => {
|
| 44 |
if (!open) return;
|
| 45 |
const userId = getUserId();
|
| 46 |
if (!userId) return;
|
| 47 |
loadDocuments(userId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
}, [open]);
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
const loadDocuments = async (userId: string) => {
|
| 51 |
setLoadingDocs(true);
|
| 52 |
setDocsError(null);
|
|
@@ -77,13 +158,12 @@ export default function KnowledgeManagement({
|
|
| 77 |
const newDoc: ApiDocument = {
|
| 78 |
id: uploadRes.data.id,
|
| 79 |
filename: uploadRes.data.filename,
|
| 80 |
-
status:
|
| 81 |
file_size: file.size,
|
| 82 |
file_type: file.name.split(".").pop() ?? "",
|
| 83 |
created_at: new Date().toISOString(),
|
| 84 |
};
|
| 85 |
setDocuments((prev) => [newDoc, ...prev]);
|
| 86 |
-
|
| 87 |
await processDocumentById(userId, uploadRes.data.id);
|
| 88 |
} catch (err) {
|
| 89 |
setUploadError(err instanceof Error ? err.message : "Upload failed");
|
|
@@ -148,6 +228,59 @@ export default function KnowledgeManagement({
|
|
| 148 |
setDocuments([]);
|
| 149 |
};
|
| 150 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
const formatFileSize = (bytes: number) => {
|
| 152 |
if (bytes < 1024) return bytes + " B";
|
| 153 |
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB";
|
|
@@ -177,7 +310,6 @@ export default function KnowledgeManagement({
|
|
| 177 |
);
|
| 178 |
}
|
| 179 |
|
| 180 |
-
// pending or failed
|
| 181 |
return (
|
| 182 |
<button
|
| 183 |
onClick={() => {
|
|
@@ -195,142 +327,422 @@ export default function KnowledgeManagement({
|
|
| 195 |
|
| 196 |
if (!open) return null;
|
| 197 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
return (
|
| 199 |
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/
|
| 200 |
-
<div className="bg-white rounded-
|
|
|
|
| 201 |
{/* Header */}
|
| 202 |
-
<div className="flex items-center justify-between
|
| 203 |
-
<div className="flex items-center gap-
|
| 204 |
-
|
| 205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
</div>
|
| 207 |
<button
|
| 208 |
-
onClick={
|
| 209 |
-
className="text-
|
| 210 |
>
|
| 211 |
-
<X className="w-
|
| 212 |
</button>
|
| 213 |
</div>
|
| 214 |
|
| 215 |
{/* Content */}
|
| 216 |
-
<div className="flex-1 overflow-y-auto
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
<
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
<
|
| 228 |
-
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
</p>
|
| 231 |
-
|
| 232 |
-
<input
|
| 233 |
-
id="file-upload"
|
| 234 |
-
type="file"
|
| 235 |
-
accept=".pdf,.docx,.txt"
|
| 236 |
-
multiple
|
| 237 |
-
onChange={handleFileUpload}
|
| 238 |
-
className="hidden"
|
| 239 |
-
disabled={uploading}
|
| 240 |
-
/>
|
| 241 |
-
</label>
|
| 242 |
-
{uploadError && (
|
| 243 |
-
<p className="mt-2 text-xs text-red-600 bg-red-50 border border-red-200 px-3 py-2 rounded-lg">
|
| 244 |
-
{uploadError}
|
| 245 |
-
</p>
|
| 246 |
-
)}
|
| 247 |
-
</div>
|
| 248 |
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
<
|
| 257 |
-
|
| 258 |
-
className="text-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
)}
|
| 264 |
-
</div>
|
| 265 |
|
| 266 |
-
|
| 267 |
-
<div
|
| 268 |
-
<
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
className="
|
| 289 |
-
|
| 290 |
-
<div className="
|
| 291 |
-
<
|
| 292 |
-
|
| 293 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
<div className="flex-1 min-w-0">
|
| 295 |
-
<
|
| 296 |
{doc.filename}
|
| 297 |
-
</h4>
|
| 298 |
-
<p className="text-xs text-slate-500 mt-0.5">
|
| 299 |
-
{formatFileSize(doc.file_size)} •{" "}
|
| 300 |
-
{formatDate(doc.created_at)}
|
| 301 |
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
</div>
|
| 303 |
-
<button
|
| 304 |
-
onClick={() => handleDeleteDocument(doc.id)}
|
| 305 |
-
disabled={deleting === doc.id}
|
| 306 |
-
className="text-slate-400 hover:text-red-600 transition flex-shrink-0 disabled:opacity-50"
|
| 307 |
-
title="Delete document"
|
| 308 |
-
>
|
| 309 |
-
{deleting === doc.id ? (
|
| 310 |
-
<Loader2 className="w-4 h-4 animate-spin" />
|
| 311 |
-
) : (
|
| 312 |
-
<Trash2 className="w-4 h-4" />
|
| 313 |
-
)}
|
| 314 |
-
</button>
|
| 315 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
|
| 317 |
-
|
| 318 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
</div>
|
| 320 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
</div>
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
</div>
|
| 327 |
|
| 328 |
{/* Footer */}
|
| 329 |
-
<div className="
|
| 330 |
-
<p className="text-[10px] text-slate-
|
| 331 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
</p>
|
| 333 |
</div>
|
|
|
|
| 334 |
</div>
|
| 335 |
</div>
|
| 336 |
);
|
|
|
|
| 7 |
Loader2,
|
| 8 |
Database,
|
| 9 |
X,
|
| 10 |
+
ChevronLeft,
|
| 11 |
+
Link,
|
| 12 |
} from "lucide-react";
|
| 13 |
+
import { toast } from "sonner";
|
| 14 |
import {
|
| 15 |
getDocuments,
|
| 16 |
uploadDocument,
|
| 17 |
processDocument,
|
| 18 |
deleteDocument,
|
| 19 |
+
getDocumentTypes,
|
| 20 |
+
getDatabaseClientTypes,
|
| 21 |
+
connectDatabase,
|
| 22 |
+
getDatabaseClients,
|
| 23 |
+
deleteDatabaseClient,
|
| 24 |
+
ingestDatabaseClient,
|
| 25 |
type ApiDocument,
|
| 26 |
type DocumentStatus,
|
| 27 |
+
type DocTypeInfo,
|
| 28 |
+
type DbType,
|
| 29 |
+
type DbTypeInfo,
|
| 30 |
+
type DatabaseClient,
|
| 31 |
} from "../../services/api";
|
| 32 |
|
| 33 |
interface KnowledgeManagementProps {
|
|
|
|
| 35 |
onClose: () => void;
|
| 36 |
}
|
| 37 |
|
| 38 |
+
type View = "main" | "db-select" | "db-credentials";
|
| 39 |
+
|
| 40 |
+
const LOGO_MAP: Record<string, string> = {
|
| 41 |
+
postgres: "https://cdn.simpleicons.org/postgresql/336791",
|
| 42 |
+
mysql: "https://cdn.simpleicons.org/mysql/4479A1",
|
| 43 |
+
supabase: "https://cdn.simpleicons.org/supabase/3ECF8E",
|
| 44 |
+
sqlserver: "https://cdn.jsdelivr.net/gh/devicons/devicon@latest/icons/microsoftsqlserver/microsoftsqlserver-plain.svg",
|
| 45 |
+
bigquery: "https://cdn.simpleicons.org/googlebigquery/4285F4",
|
| 46 |
+
snowflake: "https://cdn.simpleicons.org/snowflake/29B5E8",
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
const getUserId = (): string | null => {
|
| 50 |
const stored = localStorage.getItem("chatbot_user");
|
| 51 |
if (!stored) return null;
|
|
|
|
| 56 |
open,
|
| 57 |
onClose,
|
| 58 |
}: KnowledgeManagementProps) {
|
| 59 |
+
// ── Document state ──────────────────────────────────────────────────────────
|
| 60 |
+
const [docTypes, setDocTypes] = useState<DocTypeInfo[]>([]);
|
| 61 |
const [documents, setDocuments] = useState<ApiDocument[]>([]);
|
| 62 |
const [loadingDocs, setLoadingDocs] = useState(false);
|
| 63 |
const [docsError, setDocsError] = useState<string | null>(null);
|
|
|
|
| 66 |
const [processing, setProcessing] = useState<string | null>(null);
|
| 67 |
const [deleting, setDeleting] = useState<string | null>(null);
|
| 68 |
|
| 69 |
+
// ── Navigation state ────────────────────────────────────────────────────────
|
| 70 |
+
const [view, setView] = useState<View>("main");
|
| 71 |
+
const [selectedDbType, setSelectedDbType] = useState<DbType | null>(null);
|
| 72 |
+
|
| 73 |
+
// ── DB type & client state ──────────────────────────────────────────────────
|
| 74 |
+
const [dbTypeInfos, setDbTypeInfos] = useState<DbTypeInfo[]>([]);
|
| 75 |
+
const [dbClients, setDbClients] = useState<DatabaseClient[]>([]);
|
| 76 |
+
const [loadingDbTypes, setLoadingDbTypes] = useState(false);
|
| 77 |
+
const [ingesting, setIngesting] = useState<string | null>(null);
|
| 78 |
+
const [deletingClient, setDeletingClient] = useState<string | null>(null);
|
| 79 |
+
|
| 80 |
+
// ── DB credentials form state ───────────────────────────────────────────────
|
| 81 |
+
const [connectionName, setConnectionName] = useState("");
|
| 82 |
+
const [dbForm, setDbForm] = useState<Record<string, string | number | boolean>>({});
|
| 83 |
+
const [connecting, setConnecting] = useState(false);
|
| 84 |
+
|
| 85 |
useEffect(() => {
|
| 86 |
if (!open) return;
|
| 87 |
const userId = getUserId();
|
| 88 |
if (!userId) return;
|
| 89 |
loadDocuments(userId);
|
| 90 |
+
loadDbData(userId);
|
| 91 |
+
getDocumentTypes()
|
| 92 |
+
.then((types) => setDocTypes(types.filter((t) => t.status === "active")))
|
| 93 |
+
.catch(() =>
|
| 94 |
+
setDocTypes([
|
| 95 |
+
{ doc_type: "pdf", max_size: 10, status: "active", message: null },
|
| 96 |
+
{ doc_type: "csv", max_size: 10, status: "active", message: null },
|
| 97 |
+
{ doc_type: "xlsx", max_size: 10, status: "active", message: null },
|
| 98 |
+
])
|
| 99 |
+
);
|
| 100 |
}, [open]);
|
| 101 |
|
| 102 |
+
const acceptedExtensions = docTypes.map((t) => `.${t.doc_type}`).join(",");
|
| 103 |
+
const supportedFormatsText = docTypes.map((t) => t.doc_type.toUpperCase()).join(", ");
|
| 104 |
+
|
| 105 |
+
const loadDbData = async (userId: string) => {
|
| 106 |
+
setLoadingDbTypes(true);
|
| 107 |
+
try {
|
| 108 |
+
const [types, clients] = await Promise.all([
|
| 109 |
+
getDatabaseClientTypes(),
|
| 110 |
+
getDatabaseClients(userId),
|
| 111 |
+
]);
|
| 112 |
+
setDbTypeInfos(types);
|
| 113 |
+
setDbClients(clients);
|
| 114 |
+
} catch {
|
| 115 |
+
// non-blocking; silently fail
|
| 116 |
+
} finally {
|
| 117 |
+
setLoadingDbTypes(false);
|
| 118 |
+
}
|
| 119 |
+
};
|
| 120 |
+
|
| 121 |
+
const handleClose = () => {
|
| 122 |
+
setView("main");
|
| 123 |
+
setSelectedDbType(null);
|
| 124 |
+
setConnectionName("");
|
| 125 |
+
setDbForm({});
|
| 126 |
+
onClose();
|
| 127 |
+
};
|
| 128 |
+
|
| 129 |
+
// ── Document handlers ───────────────────────────────────────────────────────
|
| 130 |
+
|
| 131 |
const loadDocuments = async (userId: string) => {
|
| 132 |
setLoadingDocs(true);
|
| 133 |
setDocsError(null);
|
|
|
|
| 158 |
const newDoc: ApiDocument = {
|
| 159 |
id: uploadRes.data.id,
|
| 160 |
filename: uploadRes.data.filename,
|
| 161 |
+
status: uploadRes.data.status,
|
| 162 |
file_size: file.size,
|
| 163 |
file_type: file.name.split(".").pop() ?? "",
|
| 164 |
created_at: new Date().toISOString(),
|
| 165 |
};
|
| 166 |
setDocuments((prev) => [newDoc, ...prev]);
|
|
|
|
| 167 |
await processDocumentById(userId, uploadRes.data.id);
|
| 168 |
} catch (err) {
|
| 169 |
setUploadError(err instanceof Error ? err.message : "Upload failed");
|
|
|
|
| 228 |
setDocuments([]);
|
| 229 |
};
|
| 230 |
|
| 231 |
+
// ── DB handlers ─────────────────────────────────────────────────────────────
|
| 232 |
+
|
| 233 |
+
const handleDbConnect = async () => {
|
| 234 |
+
const userId = getUserId();
|
| 235 |
+
if (!userId || !selectedDbType || !connectionName.trim()) return;
|
| 236 |
+
setConnecting(true);
|
| 237 |
+
try {
|
| 238 |
+
await connectDatabase(userId, selectedDbType, connectionName.trim(), dbForm);
|
| 239 |
+
const clients = await getDatabaseClients(userId);
|
| 240 |
+
setDbClients(clients);
|
| 241 |
+
toast.success("Database connected successfully");
|
| 242 |
+
setView("main");
|
| 243 |
+
setConnectionName("");
|
| 244 |
+
setDbForm({});
|
| 245 |
+
} catch (err) {
|
| 246 |
+
toast.error(
|
| 247 |
+
err instanceof Error ? err.message : "Failed to connect to database"
|
| 248 |
+
);
|
| 249 |
+
} finally {
|
| 250 |
+
setConnecting(false);
|
| 251 |
+
}
|
| 252 |
+
};
|
| 253 |
+
|
| 254 |
+
const handleIngest = async (clientId: string) => {
|
| 255 |
+
const userId = getUserId();
|
| 256 |
+
if (!userId) return;
|
| 257 |
+
setIngesting(clientId);
|
| 258 |
+
try {
|
| 259 |
+
const res = await ingestDatabaseClient(clientId, userId);
|
| 260 |
+
toast.success(`Ingested ${res.chunks_ingested} chunks successfully`);
|
| 261 |
+
} catch (err) {
|
| 262 |
+
toast.error(err instanceof Error ? err.message : "Ingestion failed");
|
| 263 |
+
} finally {
|
| 264 |
+
setIngesting(null);
|
| 265 |
+
}
|
| 266 |
+
};
|
| 267 |
+
|
| 268 |
+
const handleDeleteClient = async (clientId: string) => {
|
| 269 |
+
const userId = getUserId();
|
| 270 |
+
if (!userId) return;
|
| 271 |
+
setDeletingClient(clientId);
|
| 272 |
+
try {
|
| 273 |
+
await deleteDatabaseClient(clientId, userId);
|
| 274 |
+
setDbClients((prev) => prev.filter((c) => c.id !== clientId));
|
| 275 |
+
} catch (err) {
|
| 276 |
+
toast.error(err instanceof Error ? err.message : "Failed to delete connection");
|
| 277 |
+
} finally {
|
| 278 |
+
setDeletingClient(null);
|
| 279 |
+
}
|
| 280 |
+
};
|
| 281 |
+
|
| 282 |
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
| 283 |
+
|
| 284 |
const formatFileSize = (bytes: number) => {
|
| 285 |
if (bytes < 1024) return bytes + " B";
|
| 286 |
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB";
|
|
|
|
| 310 |
);
|
| 311 |
}
|
| 312 |
|
|
|
|
| 313 |
return (
|
| 314 |
<button
|
| 315 |
onClick={() => {
|
|
|
|
| 327 |
|
| 328 |
if (!open) return null;
|
| 329 |
|
| 330 |
+
// ── Header title & back button logic ────────────────────────────────────────
|
| 331 |
+
|
| 332 |
+
const selectedDbInfo = dbTypeInfos.find((d) => d.db_type === selectedDbType);
|
| 333 |
+
|
| 334 |
+
const headerTitle =
|
| 335 |
+
view === "db-select"
|
| 336 |
+
? "Connect Database"
|
| 337 |
+
: view === "db-credentials"
|
| 338 |
+
? `Connect to ${selectedDbInfo?.display_name ?? selectedDbType}`
|
| 339 |
+
: "Knowledge Base";
|
| 340 |
+
|
| 341 |
+
const headerBack =
|
| 342 |
+
view === "db-select"
|
| 343 |
+
? () => setView("main")
|
| 344 |
+
: view === "db-credentials"
|
| 345 |
+
? () => setView("db-select")
|
| 346 |
+
: null;
|
| 347 |
+
|
| 348 |
+
// ── Render ───────────────────────────────────────────────────────────────────
|
| 349 |
+
|
| 350 |
return (
|
| 351 |
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm p-4">
|
| 352 |
+
<div className="bg-white rounded-2xl shadow-xl w-full max-w-md max-h-[85vh] flex flex-col">
|
| 353 |
+
|
| 354 |
{/* Header */}
|
| 355 |
+
<div className="flex items-center justify-between px-5 py-4 border-b border-slate-100">
|
| 356 |
+
<div className="flex items-center gap-3">
|
| 357 |
+
{headerBack ? (
|
| 358 |
+
<button
|
| 359 |
+
onClick={headerBack}
|
| 360 |
+
className="p-1.5 -ml-1 rounded-lg text-slate-400 hover:text-slate-700 hover:bg-slate-100 transition"
|
| 361 |
+
aria-label="Back"
|
| 362 |
+
>
|
| 363 |
+
<ChevronLeft className="w-4 h-4" />
|
| 364 |
+
</button>
|
| 365 |
+
) : (
|
| 366 |
+
<div className="w-7 h-7 rounded-lg bg-orange-100 flex items-center justify-center">
|
| 367 |
+
<Database className="w-4 h-4 text-[#FF8F00]" />
|
| 368 |
+
</div>
|
| 369 |
+
)}
|
| 370 |
+
<h2 className="text-sm font-semibold text-slate-900">{headerTitle}</h2>
|
| 371 |
</div>
|
| 372 |
<button
|
| 373 |
+
onClick={handleClose}
|
| 374 |
+
className="p-1.5 rounded-lg text-slate-400 hover:text-slate-700 hover:bg-slate-100 transition"
|
| 375 |
>
|
| 376 |
+
<X className="w-4 h-4" />
|
| 377 |
</button>
|
| 378 |
</div>
|
| 379 |
|
| 380 |
{/* Content */}
|
| 381 |
+
<div className="flex-1 overflow-y-auto px-5 py-4">
|
| 382 |
+
|
| 383 |
+
{/* ── VIEW: main ── */}
|
| 384 |
+
{view === "main" && (
|
| 385 |
+
<div className="space-y-4">
|
| 386 |
+
|
| 387 |
+
{/* Upload zone */}
|
| 388 |
+
<label
|
| 389 |
+
htmlFor="file-upload"
|
| 390 |
+
className="group flex flex-col items-center gap-2.5 border-2 border-dashed border-slate-200 rounded-xl p-7 cursor-pointer hover:border-[#FF8F00] hover:bg-orange-50/30 transition-colors"
|
| 391 |
+
>
|
| 392 |
+
<div className="w-10 h-10 rounded-xl bg-slate-100 group-hover:bg-orange-100 flex items-center justify-center transition-colors">
|
| 393 |
+
{uploading ? (
|
| 394 |
+
<Loader2 className="w-5 h-5 text-[#FF8F00] animate-spin" />
|
| 395 |
+
) : (
|
| 396 |
+
<Upload className="w-5 h-5 text-slate-400 group-hover:text-[#FF8F00] transition-colors" />
|
| 397 |
+
)}
|
| 398 |
+
</div>
|
| 399 |
+
<div className="text-center">
|
| 400 |
+
<p className="text-sm font-medium text-slate-700">
|
| 401 |
+
{uploading ? "Uploading…" : (
|
| 402 |
+
<>Drop files, or <span className="text-[#FF8F00]">browse</span></>
|
| 403 |
+
)}
|
| 404 |
+
</p>
|
| 405 |
+
<p className="text-xs text-slate-400 mt-0.5">{supportedFormatsText}</p>
|
| 406 |
+
</div>
|
| 407 |
+
<input
|
| 408 |
+
id="file-upload"
|
| 409 |
+
type="file"
|
| 410 |
+
accept={acceptedExtensions}
|
| 411 |
+
multiple
|
| 412 |
+
onChange={handleFileUpload}
|
| 413 |
+
className="hidden"
|
| 414 |
+
disabled={uploading}
|
| 415 |
+
/>
|
| 416 |
+
</label>
|
| 417 |
+
|
| 418 |
+
{uploadError && (
|
| 419 |
+
<p className="text-xs text-red-500 bg-red-50 border border-red-100 px-3 py-2 rounded-lg">
|
| 420 |
+
{uploadError}
|
| 421 |
</p>
|
| 422 |
+
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
|
| 424 |
+
{/* Connect DB row */}
|
| 425 |
+
<button
|
| 426 |
+
onClick={() => setView("db-select")}
|
| 427 |
+
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl border border-slate-200 hover:border-slate-300 hover:bg-slate-50 transition text-left group"
|
| 428 |
+
>
|
| 429 |
+
<div className="w-8 h-8 rounded-lg bg-slate-100 group-hover:bg-slate-200 flex items-center justify-center flex-shrink-0 transition-colors">
|
| 430 |
+
<Link className="w-4 h-4 text-slate-500" />
|
| 431 |
+
</div>
|
| 432 |
+
<div className="flex-1 min-w-0">
|
| 433 |
+
<p className="text-sm font-medium text-slate-700">Connect a Database</p>
|
| 434 |
+
<p className="text-xs text-slate-400">PostgreSQL and more</p>
|
| 435 |
+
</div>
|
| 436 |
+
<ChevronLeft className="w-4 h-4 text-slate-300 rotate-180 flex-shrink-0" />
|
| 437 |
+
</button>
|
| 438 |
+
|
| 439 |
+
{/* Database connections list */}
|
| 440 |
+
{dbClients.length > 0 && (
|
| 441 |
+
<div>
|
| 442 |
+
<div className="flex items-center justify-between mb-2">
|
| 443 |
+
<span className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">
|
| 444 |
+
Databases · {dbClients.length}
|
| 445 |
+
</span>
|
| 446 |
+
</div>
|
| 447 |
+
<div className="space-y-1">
|
| 448 |
+
{dbClients.map((client) => (
|
| 449 |
+
<div
|
| 450 |
+
key={client.id}
|
| 451 |
+
className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-slate-50 transition group"
|
| 452 |
+
>
|
| 453 |
+
<div className="w-8 h-8 rounded-lg bg-slate-100 flex items-center justify-center flex-shrink-0">
|
| 454 |
+
<img
|
| 455 |
+
src={LOGO_MAP[client.db_type] ?? ""}
|
| 456 |
+
alt={client.db_type}
|
| 457 |
+
className="w-5 h-5 object-contain"
|
| 458 |
+
/>
|
| 459 |
+
</div>
|
| 460 |
+
<div className="flex-1 min-w-0">
|
| 461 |
+
<p className="text-sm font-medium text-slate-800 truncate">{client.name}</p>
|
| 462 |
+
<p className="text-xs text-slate-400 capitalize">{client.db_type} · {client.status}</p>
|
| 463 |
+
</div>
|
| 464 |
+
<div className="flex items-center gap-1.5 flex-shrink-0">
|
| 465 |
+
<button
|
| 466 |
+
onClick={() => handleIngest(client.id)}
|
| 467 |
+
disabled={ingesting === client.id || client.status === "inactive"}
|
| 468 |
+
title="Ingest schema to knowledge base"
|
| 469 |
+
className="flex items-center gap-1 text-xs px-2.5 py-1 rounded-lg bg-slate-100 hover:bg-orange-100 hover:text-[#FF8F00] text-slate-500 transition disabled:opacity-40 disabled:cursor-not-allowed"
|
| 470 |
+
>
|
| 471 |
+
{ingesting === client.id ? (
|
| 472 |
+
<Loader2 className="w-3 h-3 animate-spin" />
|
| 473 |
+
) : (
|
| 474 |
+
<Database className="w-3 h-3" />
|
| 475 |
+
)}
|
| 476 |
+
{ingesting === client.id ? "Ingesting…" : "Ingest"}
|
| 477 |
+
</button>
|
| 478 |
+
<button
|
| 479 |
+
onClick={() => handleDeleteClient(client.id)}
|
| 480 |
+
disabled={deletingClient === client.id}
|
| 481 |
+
className="opacity-0 group-hover:opacity-100 text-slate-300 hover:text-red-500 transition disabled:opacity-30"
|
| 482 |
+
title="Delete connection"
|
| 483 |
+
>
|
| 484 |
+
{deletingClient === client.id ? (
|
| 485 |
+
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
| 486 |
+
) : (
|
| 487 |
+
<Trash2 className="w-3.5 h-3.5" />
|
| 488 |
+
)}
|
| 489 |
+
</button>
|
| 490 |
+
</div>
|
| 491 |
+
</div>
|
| 492 |
+
))}
|
| 493 |
+
</div>
|
| 494 |
+
</div>
|
| 495 |
)}
|
|
|
|
| 496 |
|
| 497 |
+
{/* Documents list */}
|
| 498 |
+
<div>
|
| 499 |
+
<div className="flex items-center justify-between mb-2">
|
| 500 |
+
<span className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">
|
| 501 |
+
Documents · {documents.length}
|
| 502 |
+
</span>
|
| 503 |
+
{documents.length > 0 && (
|
| 504 |
+
<button
|
| 505 |
+
onClick={deleteAllDocuments}
|
| 506 |
+
className="text-xs text-slate-400 hover:text-red-500 flex items-center gap-1 transition"
|
| 507 |
+
>
|
| 508 |
+
<Trash2 className="w-3 h-3" />
|
| 509 |
+
Clear all
|
| 510 |
+
</button>
|
| 511 |
+
)}
|
| 512 |
+
</div>
|
| 513 |
+
|
| 514 |
+
{loadingDocs ? (
|
| 515 |
+
<div className="flex justify-center py-10">
|
| 516 |
+
<Loader2 className="w-5 h-5 animate-spin text-slate-300" />
|
| 517 |
+
</div>
|
| 518 |
+
) : docsError ? (
|
| 519 |
+
<p className="text-center text-xs text-red-500 py-6">{docsError}</p>
|
| 520 |
+
) : documents.length === 0 ? (
|
| 521 |
+
<div className="text-center py-10">
|
| 522 |
+
<div className="w-12 h-12 rounded-2xl bg-slate-100 flex items-center justify-center mx-auto mb-3">
|
| 523 |
+
<FileText className="w-5 h-5 text-slate-300" />
|
| 524 |
+
</div>
|
| 525 |
+
<p className="text-sm text-slate-400">No documents yet</p>
|
| 526 |
+
<p className="text-xs text-slate-300 mt-0.5">Upload files to get started</p>
|
| 527 |
+
</div>
|
| 528 |
+
) : (
|
| 529 |
+
<div className="space-y-1">
|
| 530 |
+
{documents.map((doc) => (
|
| 531 |
+
<div
|
| 532 |
+
key={doc.id}
|
| 533 |
+
className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-slate-50 transition group"
|
| 534 |
+
>
|
| 535 |
+
<div className="w-8 h-8 rounded-lg bg-red-50 flex items-center justify-center flex-shrink-0">
|
| 536 |
+
<FileText className="w-4 h-4 text-red-400" />
|
| 537 |
+
</div>
|
| 538 |
<div className="flex-1 min-w-0">
|
| 539 |
+
<p className="text-sm font-medium text-slate-800 truncate" title={doc.filename}>
|
| 540 |
{doc.filename}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 541 |
</p>
|
| 542 |
+
<p className="text-xs text-slate-400">
|
| 543 |
+
{formatFileSize(doc.file_size)} · {formatDate(doc.created_at)}
|
| 544 |
+
</p>
|
| 545 |
+
</div>
|
| 546 |
+
<div className="flex items-center gap-2 flex-shrink-0">
|
| 547 |
+
{renderStatus(doc)}
|
| 548 |
+
<button
|
| 549 |
+
onClick={() => handleDeleteDocument(doc.id)}
|
| 550 |
+
disabled={deleting === doc.id}
|
| 551 |
+
className="opacity-0 group-hover:opacity-100 text-slate-300 hover:text-red-500 transition disabled:opacity-30"
|
| 552 |
+
title="Delete"
|
| 553 |
+
>
|
| 554 |
+
{deleting === doc.id ? (
|
| 555 |
+
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
| 556 |
+
) : (
|
| 557 |
+
<Trash2 className="w-3.5 h-3.5" />
|
| 558 |
+
)}
|
| 559 |
+
</button>
|
| 560 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 561 |
</div>
|
| 562 |
+
))}
|
| 563 |
+
</div>
|
| 564 |
+
)}
|
| 565 |
+
</div>
|
| 566 |
+
</div>
|
| 567 |
+
)}
|
| 568 |
+
|
| 569 |
+
{/* ── VIEW: db-select ── */}
|
| 570 |
+
{view === "db-select" && (
|
| 571 |
+
<div className="space-y-4">
|
| 572 |
+
<p className="text-sm text-slate-400">
|
| 573 |
+
Choose a database to connect to your knowledge base.
|
| 574 |
+
</p>
|
| 575 |
+
{loadingDbTypes ? (
|
| 576 |
+
<div className="flex justify-center py-10">
|
| 577 |
+
<Loader2 className="w-5 h-5 animate-spin text-slate-300" />
|
| 578 |
+
</div>
|
| 579 |
+
) : (
|
| 580 |
+
<div className="grid grid-cols-3 gap-2">
|
| 581 |
+
{dbTypeInfos.map((info) => {
|
| 582 |
+
const active = info.status === "active";
|
| 583 |
+
return (
|
| 584 |
+
<button
|
| 585 |
+
key={info.db_type}
|
| 586 |
+
disabled={!active}
|
| 587 |
+
onClick={() => {
|
| 588 |
+
if (!active) return;
|
| 589 |
+
setSelectedDbType(info.db_type);
|
| 590 |
+
const defaults = Object.fromEntries(
|
| 591 |
+
info.fields.map((f) => [
|
| 592 |
+
f.name,
|
| 593 |
+
f.default != null
|
| 594 |
+
? f.default
|
| 595 |
+
: f.type === "integer" ? 0 : f.type === "boolean" ? false : "",
|
| 596 |
+
])
|
| 597 |
+
);
|
| 598 |
+
setDbForm(defaults);
|
| 599 |
+
setView("db-credentials");
|
| 600 |
+
}}
|
| 601 |
+
className={[
|
| 602 |
+
"relative flex flex-col items-center gap-2 p-4 rounded-xl border transition text-xs font-medium",
|
| 603 |
+
active
|
| 604 |
+
? "border-slate-200 hover:border-[#FF8F00] hover:bg-orange-50/50 text-slate-700 cursor-pointer"
|
| 605 |
+
: "border-slate-100 bg-slate-50/60 text-slate-400 cursor-not-allowed",
|
| 606 |
+
].join(" ")}
|
| 607 |
+
>
|
| 608 |
+
<img
|
| 609 |
+
src={LOGO_MAP[info.logo] ?? ""}
|
| 610 |
+
alt={info.display_name}
|
| 611 |
+
className={`w-8 h-8 object-contain ${!active ? "opacity-30 grayscale" : ""}`}
|
| 612 |
+
/>
|
| 613 |
+
<span>{info.display_name}</span>
|
| 614 |
+
{!active && (
|
| 615 |
+
<span className="absolute top-1.5 right-1.5 text-[9px] bg-slate-200 text-slate-400 px-1.5 py-0.5 rounded-full">
|
| 616 |
+
Soon
|
| 617 |
+
</span>
|
| 618 |
+
)}
|
| 619 |
+
</button>
|
| 620 |
+
);
|
| 621 |
+
})}
|
| 622 |
+
</div>
|
| 623 |
+
)}
|
| 624 |
+
</div>
|
| 625 |
+
)}
|
| 626 |
+
|
| 627 |
+
{/* ── VIEW: db-credentials ── */}
|
| 628 |
+
{view === "db-credentials" && (
|
| 629 |
+
<div className="space-y-4">
|
| 630 |
|
| 631 |
+
{/* Selected DB chip */}
|
| 632 |
+
{selectedDbInfo && (
|
| 633 |
+
<div className="flex items-center gap-2.5 px-3 py-2.5 rounded-xl bg-slate-50 border border-slate-100">
|
| 634 |
+
<img src={LOGO_MAP[selectedDbInfo.logo] ?? ""} alt={selectedDbInfo.display_name} className="w-5 h-5 object-contain" />
|
| 635 |
+
<span className="text-sm font-medium text-slate-700">{selectedDbInfo.display_name}</span>
|
| 636 |
+
</div>
|
| 637 |
+
)}
|
| 638 |
+
|
| 639 |
+
<div className="grid grid-cols-2 gap-3">
|
| 640 |
+
{/* Connection Name */}
|
| 641 |
+
<div className="col-span-2">
|
| 642 |
+
<label className="block text-xs font-medium text-slate-500 mb-1">
|
| 643 |
+
Connection Name <span className="text-red-400">*</span>
|
| 644 |
+
</label>
|
| 645 |
+
<input
|
| 646 |
+
type="text"
|
| 647 |
+
placeholder="Production DB"
|
| 648 |
+
value={connectionName}
|
| 649 |
+
onChange={(e) => setConnectionName(e.target.value)}
|
| 650 |
+
className="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-[#FF8F00]/25 focus:border-[#FF8F00] placeholder:text-slate-300 transition"
|
| 651 |
+
/>
|
| 652 |
+
</div>
|
| 653 |
+
|
| 654 |
+
{/* Dynamic fields from API */}
|
| 655 |
+
{selectedDbInfo?.fields.map((field) => (
|
| 656 |
+
<div
|
| 657 |
+
key={field.name}
|
| 658 |
+
className={field.name === "host" || field.name === "service_account_json" ? "col-span-2" : ""}
|
| 659 |
+
>
|
| 660 |
+
<label className="block text-xs font-medium text-slate-500 mb-1 capitalize">
|
| 661 |
+
{field.name.replace(/_/g, " ")}
|
| 662 |
+
{field.required && <span className="text-red-400 ml-0.5">*</span>}
|
| 663 |
+
</label>
|
| 664 |
+
|
| 665 |
+
{field.type === "select" ? (
|
| 666 |
+
<select
|
| 667 |
+
value={String(dbForm[field.name] ?? field.default ?? "")}
|
| 668 |
+
onChange={(e) => setDbForm((f) => ({ ...f, [field.name]: e.target.value }))}
|
| 669 |
+
className="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-[#FF8F00]/25 focus:border-[#FF8F00] transition"
|
| 670 |
+
>
|
| 671 |
+
{field.options?.map((opt) => (
|
| 672 |
+
<option key={opt} value={opt}>{opt}</option>
|
| 673 |
+
))}
|
| 674 |
+
</select>
|
| 675 |
+
) : field.type === "boolean" ? (
|
| 676 |
+
<div className="flex items-center gap-2 pt-1">
|
| 677 |
+
<input
|
| 678 |
+
type="checkbox"
|
| 679 |
+
id={`field-${field.name}`}
|
| 680 |
+
checked={Boolean(dbForm[field.name] ?? field.default ?? false)}
|
| 681 |
+
onChange={(e) => setDbForm((f) => ({ ...f, [field.name]: e.target.checked }))}
|
| 682 |
+
className="w-4 h-4 accent-[#FF8F00]"
|
| 683 |
+
/>
|
| 684 |
+
<label htmlFor={`field-${field.name}`} className="text-xs text-slate-500">
|
| 685 |
+
{field.description}
|
| 686 |
+
</label>
|
| 687 |
</div>
|
| 688 |
+
) : field.name === "service_account_json" ? (
|
| 689 |
+
<textarea
|
| 690 |
+
rows={4}
|
| 691 |
+
placeholder={field.description}
|
| 692 |
+
value={String(dbForm[field.name] ?? "")}
|
| 693 |
+
onChange={(e) => setDbForm((f) => ({ ...f, [field.name]: e.target.value }))}
|
| 694 |
+
className="w-full border border-slate-200 rounded-lg px-3 py-2 text-xs bg-white focus:outline-none focus:ring-2 focus:ring-[#FF8F00]/25 focus:border-[#FF8F00] placeholder:text-slate-300 transition font-mono"
|
| 695 |
+
/>
|
| 696 |
+
) : (
|
| 697 |
+
<input
|
| 698 |
+
type={field.sensitive ? "password" : field.type === "integer" ? "number" : "text"}
|
| 699 |
+
placeholder={field.default != null ? String(field.default) : field.description}
|
| 700 |
+
value={String(dbForm[field.name] ?? "")}
|
| 701 |
+
onChange={(e) =>
|
| 702 |
+
setDbForm((f) => ({
|
| 703 |
+
...f,
|
| 704 |
+
[field.name]: field.type === "integer"
|
| 705 |
+
? (parseInt(e.target.value, 10) || 0)
|
| 706 |
+
: e.target.value,
|
| 707 |
+
}))
|
| 708 |
+
}
|
| 709 |
+
className="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-[#FF8F00]/25 focus:border-[#FF8F00] placeholder:text-slate-300 transition"
|
| 710 |
+
/>
|
| 711 |
+
)}
|
| 712 |
</div>
|
| 713 |
+
))}
|
| 714 |
+
</div>
|
| 715 |
+
|
| 716 |
+
<button
|
| 717 |
+
onClick={handleDbConnect}
|
| 718 |
+
disabled={
|
| 719 |
+
connecting ||
|
| 720 |
+
!connectionName.trim() ||
|
| 721 |
+
(selectedDbInfo?.fields.filter((f) => f.required).some((f) => !dbForm[f.name]) ?? false)
|
| 722 |
+
}
|
| 723 |
+
className="w-full flex items-center justify-center gap-2 bg-[#FF8F00] hover:bg-[#FF6F00] active:bg-[#E65100] text-white py-2.5 rounded-xl text-sm font-medium transition disabled:opacity-40 disabled:cursor-not-allowed"
|
| 724 |
+
>
|
| 725 |
+
{connecting ? (
|
| 726 |
+
<><Loader2 className="w-4 h-4 animate-spin" /> Connecting…</>
|
| 727 |
+
) : (
|
| 728 |
+
"Connect"
|
| 729 |
+
)}
|
| 730 |
+
</button>
|
| 731 |
+
</div>
|
| 732 |
+
)}
|
| 733 |
</div>
|
| 734 |
|
| 735 |
{/* Footer */}
|
| 736 |
+
<div className="px-5 py-3 border-t border-slate-100">
|
| 737 |
+
<p className="text-[10px] text-slate-300 text-center">
|
| 738 |
+
{view === "main"
|
| 739 |
+
? `Supported formats: ${supportedFormatsText}`
|
| 740 |
+
: view === "db-select"
|
| 741 |
+
? "More integrations coming soon"
|
| 742 |
+
: "Credentials are encrypted at rest"}
|
| 743 |
</p>
|
| 744 |
</div>
|
| 745 |
+
|
| 746 |
</div>
|
| 747 |
</div>
|
| 748 |
);
|
src/app/components/Login.tsx
CHANGED
|
@@ -1,13 +1,15 @@
|
|
| 1 |
import { useState } from "react";
|
| 2 |
import { useNavigate } from "react-router";
|
| 3 |
-
import { LogIn, Loader2 } from "lucide-react";
|
| 4 |
import { login } from "../../services/api";
|
|
|
|
| 5 |
|
| 6 |
export default function Login() {
|
| 7 |
const [email, setEmail] = useState("");
|
| 8 |
const [password, setPassword] = useState("");
|
| 9 |
const [error, setError] = useState("");
|
| 10 |
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
| 11 |
const navigate = useNavigate();
|
| 12 |
|
| 13 |
const handleLogin = async (e: React.FormEvent) => {
|
|
@@ -43,28 +45,137 @@ export default function Login() {
|
|
| 43 |
};
|
| 44 |
|
| 45 |
return (
|
| 46 |
-
<div className="min-h-screen flex items-center justify-center
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
</div>
|
| 53 |
</div>
|
| 54 |
|
| 55 |
-
<
|
| 56 |
-
Welcome Back
|
| 57 |
-
</h1>
|
| 58 |
-
<p className="text-center text-slate-500 text-sm mb-6">
|
| 59 |
-
Sign in to continue to your chatbot
|
| 60 |
-
</p>
|
| 61 |
-
|
| 62 |
-
<form onSubmit={handleLogin} className="space-y-4">
|
| 63 |
<div>
|
| 64 |
-
<label
|
| 65 |
-
htmlFor="email"
|
| 66 |
-
className="block text-xs mb-1.5 text-slate-700"
|
| 67 |
-
>
|
| 68 |
Email Address
|
| 69 |
</label>
|
| 70 |
<input
|
|
@@ -72,32 +183,40 @@ export default function Login() {
|
|
| 72 |
type="email"
|
| 73 |
value={email}
|
| 74 |
onChange={(e) => setEmail(e.target.value)}
|
| 75 |
-
className="w-full px-3 py-2 text-sm rounded-
|
| 76 |
placeholder="you@example.com"
|
| 77 |
disabled={isLoading}
|
| 78 |
/>
|
| 79 |
</div>
|
| 80 |
|
| 81 |
<div>
|
| 82 |
-
<label
|
| 83 |
-
htmlFor="password"
|
| 84 |
-
className="block text-xs mb-1.5 text-slate-700"
|
| 85 |
-
>
|
| 86 |
Password
|
| 87 |
</label>
|
| 88 |
-
<
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
</div>
|
| 98 |
|
| 99 |
{error && (
|
| 100 |
-
<div className="bg-red-50 border border-red-
|
| 101 |
{error}
|
| 102 |
</div>
|
| 103 |
)}
|
|
@@ -105,14 +224,14 @@ export default function Login() {
|
|
| 105 |
<button
|
| 106 |
type="submit"
|
| 107 |
disabled={isLoading}
|
| 108 |
-
className="w-full flex items-center justify-center gap-2 bg-
|
| 109 |
>
|
| 110 |
{isLoading ? (
|
| 111 |
-
<Loader2 className="w-4 h-4 animate-spin" />
|
| 112 |
) : (
|
| 113 |
-
<LogIn className="w-4 h-4" />
|
| 114 |
)}
|
| 115 |
-
{isLoading ? "Signing in
|
| 116 |
</button>
|
| 117 |
</form>
|
| 118 |
</div>
|
|
|
|
| 1 |
import { useState } from "react";
|
| 2 |
import { useNavigate } from "react-router";
|
| 3 |
+
import { LogIn, Loader2, Eye, EyeOff } from "lucide-react";
|
| 4 |
import { login } from "../../services/api";
|
| 5 |
+
import logoUrl from "../../assets/maintiva-logo.jpg";
|
| 6 |
|
| 7 |
export default function Login() {
|
| 8 |
const [email, setEmail] = useState("");
|
| 9 |
const [password, setPassword] = useState("");
|
| 10 |
const [error, setError] = useState("");
|
| 11 |
const [isLoading, setIsLoading] = useState(false);
|
| 12 |
+
const [showPassword, setShowPassword] = useState(false);
|
| 13 |
const navigate = useNavigate();
|
| 14 |
|
| 15 |
const handleLogin = async (e: React.FormEvent) => {
|
|
|
|
| 45 |
};
|
| 46 |
|
| 47 |
return (
|
| 48 |
+
<div className="relative min-h-screen flex items-center justify-center overflow-hidden bg-[#fcfdfd]">
|
| 49 |
+
|
| 50 |
+
{/* ── Dot grid texture ── */}
|
| 51 |
+
<div
|
| 52 |
+
className="absolute inset-0 z-0 opacity-20"
|
| 53 |
+
style={{
|
| 54 |
+
backgroundImage: "radial-gradient(circle, #334155 1px, transparent 1px)",
|
| 55 |
+
backgroundSize: "28px 28px",
|
| 56 |
+
}}
|
| 57 |
+
/>
|
| 58 |
+
|
| 59 |
+
{/* ── Gradient orbs — glowing on dark bg ── */}
|
| 60 |
+
{/* Cyan — top left */}
|
| 61 |
+
<div
|
| 62 |
+
className="absolute -top-32 -left-32 w-[520px] h-[520px] rounded-full blur-3xl opacity-[0.18] animate-pulse"
|
| 63 |
+
style={{ background: "#0ea5e9", animationDuration: "5s" }}
|
| 64 |
+
/>
|
| 65 |
+
{/* Orange — bottom right */}
|
| 66 |
+
<div
|
| 67 |
+
className="absolute -bottom-40 -right-40 w-[560px] h-[560px] rounded-full blur-3xl opacity-[0.16] animate-pulse"
|
| 68 |
+
style={{ background: "#f97316", animationDuration: "7s" }}
|
| 69 |
+
/>
|
| 70 |
+
{/* Green — top right */}
|
| 71 |
+
<div
|
| 72 |
+
className="absolute -top-24 right-1/4 w-[320px] h-[320px] rounded-full blur-3xl opacity-[0.12] animate-pulse"
|
| 73 |
+
style={{ background: "#10b981", animationDuration: "9s" }}
|
| 74 |
+
/>
|
| 75 |
+
{/* Purple accent — center left */}
|
| 76 |
+
<div
|
| 77 |
+
className="absolute top-1/2 -left-40 w-[360px] h-[360px] rounded-full blur-3xl opacity-[0.10] animate-pulse"
|
| 78 |
+
style={{ background: "#8b5cf6", animationDuration: "11s" }}
|
| 79 |
+
/>
|
| 80 |
+
|
| 81 |
+
{/* ── Neural network — bottom left ── */}
|
| 82 |
+
<svg
|
| 83 |
+
className="absolute bottom-8 left-8 opacity-[0.35] animate-float-slow"
|
| 84 |
+
width="200" height="200" viewBox="0 0 200 200"
|
| 85 |
+
fill="none" xmlns="http://www.w3.org/2000/svg"
|
| 86 |
+
>
|
| 87 |
+
<line x1="20" y1="150" x2="65" y2="90" stroke="#0ea5e9" strokeWidth="1"/>
|
| 88 |
+
<line x1="20" y1="150" x2="55" y2="170" stroke="#0ea5e9" strokeWidth="1"/>
|
| 89 |
+
<line x1="65" y1="90" x2="110" y2="125" stroke="#0ea5e9" strokeWidth="1"/>
|
| 90 |
+
<line x1="65" y1="90" x2="130" y2="55" stroke="#10b981" strokeWidth="1"/>
|
| 91 |
+
<line x1="110" y1="125" x2="170" y2="105" stroke="#0ea5e9" strokeWidth="1"/>
|
| 92 |
+
<line x1="130" y1="55" x2="170" y2="105" stroke="#10b981" strokeWidth="1"/>
|
| 93 |
+
<line x1="130" y1="55" x2="175" y2="30" stroke="#10b981" strokeWidth="1"/>
|
| 94 |
+
<line x1="55" y1="170" x2="110" y2="125" stroke="#0ea5e9" strokeWidth="1"/>
|
| 95 |
+
<line x1="175" y1="30" x2="170" y2="105" stroke="#10b981" strokeWidth="1"/>
|
| 96 |
+
<circle cx="20" cy="150" r="3.5" fill="#0ea5e9" fillOpacity="0.7"/>
|
| 97 |
+
<circle cx="55" cy="170" r="2.5" fill="#0ea5e9" fillOpacity="0.5"/>
|
| 98 |
+
<circle cx="65" cy="90" r="5" fill="#0ea5e9" fillOpacity="0.9"/>
|
| 99 |
+
<circle cx="110" cy="125" r="3.5" fill="#0ea5e9" fillOpacity="0.7"/>
|
| 100 |
+
<circle cx="130" cy="55" r="5" fill="#10b981" fillOpacity="0.9"/>
|
| 101 |
+
<circle cx="170" cy="105" r="3.5" fill="#10b981" fillOpacity="0.7"/>
|
| 102 |
+
<circle cx="175" cy="30" r="2.5" fill="#10b981" fillOpacity="0.5"/>
|
| 103 |
+
</svg>
|
| 104 |
+
|
| 105 |
+
{/* ── Neural network — top right ── */}
|
| 106 |
+
<svg
|
| 107 |
+
className="absolute top-8 right-8 opacity-[0.30] animate-float"
|
| 108 |
+
style={{ "--float-rotate": "0deg" } as React.CSSProperties}
|
| 109 |
+
width="180" height="180" viewBox="0 0 180 180"
|
| 110 |
+
fill="none" xmlns="http://www.w3.org/2000/svg"
|
| 111 |
+
>
|
| 112 |
+
<line x1="160" y1="30" x2="115" y2="80" stroke="#f97316" strokeWidth="1"/>
|
| 113 |
+
<line x1="115" y1="80" x2="70" y2="55" stroke="#f97316" strokeWidth="1"/>
|
| 114 |
+
<line x1="115" y1="80" x2="130" y2="135" stroke="#0ea5e9" strokeWidth="1"/>
|
| 115 |
+
<line x1="70" y1="55" x2="25" y2="90" stroke="#f97316" strokeWidth="1"/>
|
| 116 |
+
<line x1="25" y1="90" x2="80" y2="155" stroke="#0ea5e9" strokeWidth="1"/>
|
| 117 |
+
<line x1="80" y1="155" x2="130" y2="135" stroke="#0ea5e9" strokeWidth="1"/>
|
| 118 |
+
<line x1="70" y1="55" x2="30" y2="25" stroke="#f97316" strokeWidth="1"/>
|
| 119 |
+
<circle cx="160" cy="30" r="3.5" fill="#f97316" fillOpacity="0.7"/>
|
| 120 |
+
<circle cx="115" cy="80" r="5" fill="#f97316" fillOpacity="0.9"/>
|
| 121 |
+
<circle cx="70" cy="55" r="3.5" fill="#f97316" fillOpacity="0.7"/>
|
| 122 |
+
<circle cx="130" cy="135" r="2.5" fill="#0ea5e9" fillOpacity="0.5"/>
|
| 123 |
+
<circle cx="25" cy="90" r="3.5" fill="#0ea5e9" fillOpacity="0.7"/>
|
| 124 |
+
<circle cx="80" cy="155" r="2.5" fill="#0ea5e9" fillOpacity="0.5"/>
|
| 125 |
+
<circle cx="30" cy="25" r="2.5" fill="#f97316" fillOpacity="0.5"/>
|
| 126 |
+
</svg>
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
{/* ── Rotated square — bottom left ── */}
|
| 130 |
+
<div
|
| 131 |
+
className="absolute bottom-16 left-16 w-24 h-24 border border-slate-200 opacity-50 animate-float"
|
| 132 |
+
style={{ transform: "rotate(20deg)", "--float-rotate": "20deg" } as React.CSSProperties}
|
| 133 |
+
/>
|
| 134 |
+
<div
|
| 135 |
+
className="absolute bottom-28 left-28 w-12 h-12 border border-slate-200 opacity-40"
|
| 136 |
+
style={{ transform: "rotate(45deg)" }}
|
| 137 |
+
/>
|
| 138 |
+
|
| 139 |
+
{/* ── Hexagon — bottom right ── */}
|
| 140 |
+
<svg
|
| 141 |
+
className="absolute bottom-12 right-16 opacity-[0.35] animate-float"
|
| 142 |
+
style={{ "--float-rotate": "0deg", animationDelay: "2s" } as React.CSSProperties}
|
| 143 |
+
width="80" height="80" viewBox="0 0 160 160"
|
| 144 |
+
fill="none" xmlns="http://www.w3.org/2000/svg"
|
| 145 |
+
>
|
| 146 |
+
<path
|
| 147 |
+
d="M 140 80 L 110 132 L 50 132 L 20 80 L 50 28 L 110 28 Z"
|
| 148 |
+
stroke="#f97316" strokeWidth="1.5" fill="none"
|
| 149 |
+
/>
|
| 150 |
+
<path
|
| 151 |
+
d="M 122 80 L 101 114 L 59 114 L 38 80 L 59 46 L 101 46 Z"
|
| 152 |
+
stroke="#f97316" strokeWidth="1" strokeOpacity="0.5" fill="none"
|
| 153 |
+
/>
|
| 154 |
+
</svg>
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
{/* ── Small floating dots ── */}
|
| 158 |
+
<div className="absolute top-1/4 left-12 w-2 h-2 rounded-full bg-sky-400 opacity-60 animate-float" style={{ animationDelay: "1s" }} />
|
| 159 |
+
<div className="absolute top-1/3 right-20 w-1.5 h-1.5 rounded-full bg-orange-400 opacity-60 animate-float-slow" style={{ animationDelay: "3s" }} />
|
| 160 |
+
<div className="absolute bottom-1/3 left-1/4 w-1.5 h-1.5 rounded-full bg-emerald-400 opacity-60 animate-float" style={{ animationDelay: "0.5s" }} />
|
| 161 |
+
<div className="absolute top-2/3 right-1/3 w-1 h-1 rounded-full bg-violet-400 opacity-50 animate-float-slow" style={{ animationDelay: "4s" }} />
|
| 162 |
+
|
| 163 |
+
{/* ── Login card ── */}
|
| 164 |
+
<div className="relative z-10 w-full max-w-md xl:max-w-lg 2xl:max-w-xl px-4">
|
| 165 |
+
<div className="bg-white rounded-2xl shadow-2xl shadow-black/40 p-7 xl:p-10 border border-white/10">
|
| 166 |
+
|
| 167 |
+
{/* Brand logo */}
|
| 168 |
+
<div className="flex flex-col items-center gap-2 xl:gap-4 mb-6 xl:mb-8">
|
| 169 |
+
<img src={logoUrl} alt="Maintiva" className="w-12 h-12 xl:w-20 xl:h-20 object-contain" />
|
| 170 |
+
<div className="text-center">
|
| 171 |
+
<h1 className="text-xl xl:text-3xl font-semibold text-slate-900">Maintiva Agent</h1>
|
| 172 |
+
<p className="text-slate-400 text-sm xl:text-base mt-0.5 xl:mt-1">Welcome back to your AI Based Virtual Agent for Analysis, Learning & RCA</p>
|
| 173 |
</div>
|
| 174 |
</div>
|
| 175 |
|
| 176 |
+
<form onSubmit={handleLogin} className="space-y-4 xl:space-y-5">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
<div>
|
| 178 |
+
<label htmlFor="email" className="block text-xs xl:text-sm font-medium mb-1.5 text-slate-600">
|
|
|
|
|
|
|
|
|
|
| 179 |
Email Address
|
| 180 |
</label>
|
| 181 |
<input
|
|
|
|
| 183 |
type="email"
|
| 184 |
value={email}
|
| 185 |
onChange={(e) => setEmail(e.target.value)}
|
| 186 |
+
className="w-full px-3 xl:px-4 py-2 xl:py-3 text-sm xl:text-base rounded-xl border border-slate-200 bg-slate-50 focus:outline-none focus:ring-2 focus:ring-sky-400/30 focus:border-sky-400 placeholder:text-slate-300 transition"
|
| 187 |
placeholder="you@example.com"
|
| 188 |
disabled={isLoading}
|
| 189 |
/>
|
| 190 |
</div>
|
| 191 |
|
| 192 |
<div>
|
| 193 |
+
<label htmlFor="password" className="block text-xs xl:text-sm font-medium mb-1.5 text-slate-600">
|
|
|
|
|
|
|
|
|
|
| 194 |
Password
|
| 195 |
</label>
|
| 196 |
+
<div className="relative">
|
| 197 |
+
<input
|
| 198 |
+
id="password"
|
| 199 |
+
type={showPassword ? "text" : "password"}
|
| 200 |
+
value={password}
|
| 201 |
+
onChange={(e) => setPassword(e.target.value)}
|
| 202 |
+
className="w-full px-3 xl:px-4 py-2 xl:py-3 pr-10 xl:pr-12 text-sm xl:text-base rounded-xl border border-slate-200 bg-slate-50 focus:outline-none focus:ring-2 focus:ring-sky-400/30 focus:border-sky-400 placeholder:text-slate-300 transition"
|
| 203 |
+
placeholder="Enter your password"
|
| 204 |
+
disabled={isLoading}
|
| 205 |
+
/>
|
| 206 |
+
<button
|
| 207 |
+
type="button"
|
| 208 |
+
onClick={() => setShowPassword((v) => !v)}
|
| 209 |
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 transition"
|
| 210 |
+
tabIndex={-1}
|
| 211 |
+
aria-label={showPassword ? "Hide password" : "Show password"}
|
| 212 |
+
>
|
| 213 |
+
{showPassword ? <EyeOff className="w-4 h-4 xl:w-5 xl:h-5" /> : <Eye className="w-4 h-4 xl:w-5 xl:h-5" />}
|
| 214 |
+
</button>
|
| 215 |
+
</div>
|
| 216 |
</div>
|
| 217 |
|
| 218 |
{error && (
|
| 219 |
+
<div className="bg-red-50 border border-red-100 text-red-600 px-3 py-2 rounded-xl text-xs xl:text-sm">
|
| 220 |
{error}
|
| 221 |
</div>
|
| 222 |
)}
|
|
|
|
| 224 |
<button
|
| 225 |
type="submit"
|
| 226 |
disabled={isLoading}
|
| 227 |
+
className="w-full flex items-center justify-center gap-2 bg-[#059669] hover:bg-[#047857] active:bg-[#065F46] text-white py-2.5 xl:py-3.5 text-sm xl:text-base rounded-xl transition font-medium disabled:opacity-50 disabled:cursor-not-allowed mt-1"
|
| 228 |
>
|
| 229 |
{isLoading ? (
|
| 230 |
+
<Loader2 className="w-4 h-4 xl:w-5 xl:h-5 animate-spin" />
|
| 231 |
) : (
|
| 232 |
+
<LogIn className="w-4 h-4 xl:w-5 xl:h-5" />
|
| 233 |
)}
|
| 234 |
+
{isLoading ? "Signing in…" : "Sign In"}
|
| 235 |
</button>
|
| 236 |
</form>
|
| 237 |
</div>
|
src/app/components/Main.tsx
CHANGED
|
@@ -1,23 +1,7 @@
|
|
| 1 |
-
import { useState, useEffect, useRef } from "react";
|
| 2 |
import { useNavigate } from "react-router";
|
| 3 |
-
import {
|
| 4 |
-
|
| 5 |
-
Plus,
|
| 6 |
-
Trash2,
|
| 7 |
-
LogOut,
|
| 8 |
-
Menu,
|
| 9 |
-
X,
|
| 10 |
-
MessageSquare,
|
| 11 |
-
User,
|
| 12 |
-
Bot,
|
| 13 |
-
Loader2,
|
| 14 |
-
Database,
|
| 15 |
-
} from "lucide-react";
|
| 16 |
-
import ReactMarkdown from "react-markdown";
|
| 17 |
-
import remarkGfm from "remark-gfm";
|
| 18 |
-
import remarkMath from "remark-math";
|
| 19 |
-
import rehypeKatex from "rehype-katex";
|
| 20 |
-
import type { Components } from "react-markdown";
|
| 21 |
import KnowledgeManagement from "./KnowledgeManagement";
|
| 22 |
import {
|
| 23 |
getRooms,
|
|
@@ -27,21 +11,16 @@ import {
|
|
| 27 |
streamChat,
|
| 28 |
type ChatSource,
|
| 29 |
} from "../../services/api";
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
role: "user" | "assistant";
|
| 41 |
-
content: string;
|
| 42 |
-
timestamp: number;
|
| 43 |
-
sources?: ChatSource[];
|
| 44 |
-
}
|
| 45 |
|
| 46 |
interface ChatRoom {
|
| 47 |
id: string;
|
|
@@ -52,177 +31,32 @@ interface ChatRoom {
|
|
| 52 |
messagesLoaded: boolean;
|
| 53 |
}
|
| 54 |
|
| 55 |
-
// Preprocess markdown to ensure tables are properly separated from surrounding text
|
| 56 |
-
function preprocessMarkdown(content: string): string {
|
| 57 |
-
// Step 1: Split concatenated table rows — "| val ||" means end of row + start of next
|
| 58 |
-
let result = content.replace(/\|\|/g, "|\n|");
|
| 59 |
-
|
| 60 |
-
// Step 2: Per-line — if a line has non-pipe text before a pipe table row, split them
|
| 61 |
-
const lines = result.split("\n");
|
| 62 |
-
const processed = lines.map((line) => {
|
| 63 |
-
const tableStart = line.indexOf("|");
|
| 64 |
-
if (tableStart > 0) {
|
| 65 |
-
const tableContent = line.slice(tableStart);
|
| 66 |
-
// Confirm it looks like a table row (at least 2 pipes)
|
| 67 |
-
if ((tableContent.match(/\|/g) ?? []).length >= 2) {
|
| 68 |
-
return line.slice(0, tableStart).trimEnd() + "\n\n" + tableContent;
|
| 69 |
-
}
|
| 70 |
-
}
|
| 71 |
-
return line;
|
| 72 |
-
});
|
| 73 |
-
result = processed.join("\n");
|
| 74 |
-
|
| 75 |
-
// Step 3: Ensure blank line before table rows preceded by non-table text (not another row ending with |)
|
| 76 |
-
result = result.replace(/([^|\n])\n(\|)/g, "$1\n\n$2");
|
| 77 |
-
|
| 78 |
-
return result;
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
// Markdown component overrides for clean rendering inside chat bubbles
|
| 82 |
-
const markdownComponents: Components = {
|
| 83 |
-
p: ({ children }) => (
|
| 84 |
-
<p className="text-sm mb-2 last:mb-0 leading-relaxed">{children}</p>
|
| 85 |
-
),
|
| 86 |
-
h1: ({ children }) => (
|
| 87 |
-
<h1 className="text-lg font-bold mb-3 mt-4 first:mt-0">{children}</h1>
|
| 88 |
-
),
|
| 89 |
-
h2: ({ children }) => (
|
| 90 |
-
<h2 className="text-base font-bold mb-2 mt-3 first:mt-0">{children}</h2>
|
| 91 |
-
),
|
| 92 |
-
h3: ({ children }) => (
|
| 93 |
-
<h3 className="text-sm font-semibold mb-2 mt-2 first:mt-0">{children}</h3>
|
| 94 |
-
),
|
| 95 |
-
ul: ({ children }) => (
|
| 96 |
-
<ul className="list-disc pl-5 mb-2 space-y-1 text-sm">{children}</ul>
|
| 97 |
-
),
|
| 98 |
-
ol: ({ children }) => (
|
| 99 |
-
<ol className="list-decimal pl-5 mb-2 space-y-1 text-sm">{children}</ol>
|
| 100 |
-
),
|
| 101 |
-
li: ({ children }) => <li className="text-sm leading-relaxed">{children}</li>,
|
| 102 |
-
code: ({ children, className }) => {
|
| 103 |
-
const isBlock = className?.startsWith("language-");
|
| 104 |
-
if (isBlock) {
|
| 105 |
-
return (
|
| 106 |
-
<code className="block text-xs font-mono text-slate-100 leading-relaxed">
|
| 107 |
-
{children}
|
| 108 |
-
</code>
|
| 109 |
-
);
|
| 110 |
-
}
|
| 111 |
-
return (
|
| 112 |
-
<code className="bg-slate-100 text-pink-600 px-1.5 py-0.5 rounded text-xs font-mono">
|
| 113 |
-
{children}
|
| 114 |
-
</code>
|
| 115 |
-
);
|
| 116 |
-
},
|
| 117 |
-
pre: ({ children }) => (
|
| 118 |
-
<pre className="bg-slate-900 rounded-lg p-3 mb-2 mt-1 overflow-x-auto text-xs">
|
| 119 |
-
{children}
|
| 120 |
-
</pre>
|
| 121 |
-
),
|
| 122 |
-
blockquote: ({ children }) => (
|
| 123 |
-
<blockquote className="border-l-4 border-slate-300 pl-3 text-slate-500 italic mb-2 text-sm">
|
| 124 |
-
{children}
|
| 125 |
-
</blockquote>
|
| 126 |
-
),
|
| 127 |
-
table: ({ children }) => (
|
| 128 |
-
<div className="overflow-x-auto mb-2">
|
| 129 |
-
<table className="w-full text-sm border-collapse">{children}</table>
|
| 130 |
-
</div>
|
| 131 |
-
),
|
| 132 |
-
th: ({ children }) => (
|
| 133 |
-
<th className="border border-slate-200 px-3 py-1.5 bg-slate-100 font-medium text-left text-xs">
|
| 134 |
-
{children}
|
| 135 |
-
</th>
|
| 136 |
-
),
|
| 137 |
-
td: ({ children }) => (
|
| 138 |
-
<td className="border border-slate-200 px-3 py-1.5 text-xs">{children}</td>
|
| 139 |
-
),
|
| 140 |
-
a: ({ children, href }) => (
|
| 141 |
-
<a
|
| 142 |
-
href={href}
|
| 143 |
-
target="_blank"
|
| 144 |
-
rel="noopener noreferrer"
|
| 145 |
-
className="text-blue-600 underline hover:text-blue-800"
|
| 146 |
-
>
|
| 147 |
-
{children}
|
| 148 |
-
</a>
|
| 149 |
-
),
|
| 150 |
-
strong: ({ children }) => (
|
| 151 |
-
<strong className="font-semibold">{children}</strong>
|
| 152 |
-
),
|
| 153 |
-
hr: () => <hr className="border-slate-200 my-3" />,
|
| 154 |
-
};
|
| 155 |
-
|
| 156 |
-
// Typing indicator — three bouncing dots
|
| 157 |
-
function useLoadingMessages() {
|
| 158 |
-
const [messages, setMessages] = useState<string[]>([]);
|
| 159 |
-
|
| 160 |
-
useEffect(() => {
|
| 161 |
-
fetch("/loading-messages.yaml")
|
| 162 |
-
.then((r) => r.text())
|
| 163 |
-
.then((text) => {
|
| 164 |
-
const parsed = text
|
| 165 |
-
.split("\n")
|
| 166 |
-
.filter((l) => l.trimStart().startsWith("- "))
|
| 167 |
-
.map((l) => l.replace(/^\s*- /, "").trim())
|
| 168 |
-
.filter(Boolean);
|
| 169 |
-
if (parsed.length > 0) setMessages(parsed);
|
| 170 |
-
})
|
| 171 |
-
.catch(() => {});
|
| 172 |
-
}, []);
|
| 173 |
-
|
| 174 |
-
return messages;
|
| 175 |
-
}
|
| 176 |
-
|
| 177 |
-
function TypingIndicator() {
|
| 178 |
-
const messages = useLoadingMessages();
|
| 179 |
-
const [index, setIndex] = useState(0);
|
| 180 |
-
|
| 181 |
-
useEffect(() => {
|
| 182 |
-
if (messages.length === 0) return;
|
| 183 |
-
setIndex(Math.floor(Math.random() * messages.length));
|
| 184 |
-
const id = setInterval(() => {
|
| 185 |
-
setIndex((prev) => {
|
| 186 |
-
let next: number;
|
| 187 |
-
do { next = Math.floor(Math.random() * messages.length); } while (messages.length > 1 && next === prev);
|
| 188 |
-
return next;
|
| 189 |
-
});
|
| 190 |
-
}, 300);
|
| 191 |
-
return () => clearInterval(id);
|
| 192 |
-
}, [messages]);
|
| 193 |
-
|
| 194 |
-
if (messages.length === 0) {
|
| 195 |
-
return (
|
| 196 |
-
<div className="flex gap-1.5 items-center py-1 px-0.5">
|
| 197 |
-
<span className="w-2 h-2 bg-slate-400 rounded-full animate-bounce" style={{ animationDelay: "0ms", animationDuration: "1s" }} />
|
| 198 |
-
<span className="w-2 h-2 bg-slate-400 rounded-full animate-bounce" style={{ animationDelay: "200ms", animationDuration: "1s" }} />
|
| 199 |
-
<span className="w-2 h-2 bg-slate-400 rounded-full animate-bounce" style={{ animationDelay: "400ms", animationDuration: "1s" }} />
|
| 200 |
-
</div>
|
| 201 |
-
);
|
| 202 |
-
}
|
| 203 |
-
|
| 204 |
-
return (
|
| 205 |
-
<div className="flex items-center gap-2 py-1 px-0.5 text-sm text-slate-400 italic">
|
| 206 |
-
<span className="inline-block w-2 h-2 rounded-full bg-slate-400 animate-pulse" />
|
| 207 |
-
<span>{messages[index]}…</span>
|
| 208 |
-
</div>
|
| 209 |
-
);
|
| 210 |
-
}
|
| 211 |
-
|
| 212 |
export default function Main() {
|
| 213 |
const navigate = useNavigate();
|
| 214 |
-
const [sidebarOpen, setSidebarOpen] = useState(true);
|
| 215 |
const [chats, setChats] = useState<ChatRoom[]>([]);
|
| 216 |
const [currentChatId, setCurrentChatId] = useState<string | null>(null);
|
| 217 |
-
const [input, setInput] = useState("");
|
| 218 |
const [isStreaming, setIsStreaming] = useState(false);
|
| 219 |
const [streamingMsgId, setStreamingMsgId] = useState<string | null>(null);
|
| 220 |
const [roomsLoading, setRoomsLoading] = useState(false);
|
| 221 |
-
const [roomsError, setRoomsError] = useState<string | null>(null);
|
| 222 |
-
const messagesEndRef = useRef<HTMLDivElement>(null);
|
| 223 |
const [user, setUser] = useState<StoredUser | null>(null);
|
| 224 |
const [knowledgeOpen, setKnowledgeOpen] = useState(false);
|
|
|
|
| 225 |
const abortControllerRef = useRef<AbortController | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
|
| 227 |
useEffect(() => {
|
| 228 |
const storedUser = localStorage.getItem("chatbot_user");
|
|
@@ -233,10 +67,6 @@ export default function Main() {
|
|
| 233 |
}
|
| 234 |
}, []);
|
| 235 |
|
| 236 |
-
useEffect(() => {
|
| 237 |
-
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
| 238 |
-
}, [currentChatId, chats]);
|
| 239 |
-
|
| 240 |
useEffect(() => {
|
| 241 |
if (!currentChatId) return;
|
| 242 |
const chat = chats.find((c) => c.id === currentChatId);
|
|
@@ -248,7 +78,6 @@ export default function Main() {
|
|
| 248 |
|
| 249 |
const loadRooms = async (userId: string) => {
|
| 250 |
setRoomsLoading(true);
|
| 251 |
-
setRoomsError(null);
|
| 252 |
try {
|
| 253 |
const apiRooms = await getRooms(userId);
|
| 254 |
const mapped: ChatRoom[] = apiRooms.map((r) => ({
|
|
@@ -261,105 +90,123 @@ export default function Main() {
|
|
| 261 |
}));
|
| 262 |
setChats(mapped);
|
| 263 |
if (mapped.length > 0) {
|
| 264 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
}
|
| 266 |
-
} catch
|
| 267 |
-
|
| 268 |
-
err instanceof Error ? err.message : "Failed to load chats"
|
| 269 |
-
);
|
| 270 |
} finally {
|
| 271 |
setRoomsLoading(false);
|
| 272 |
}
|
| 273 |
};
|
| 274 |
|
| 275 |
-
const loadRoomMessages = async (roomId: string) => {
|
| 276 |
try {
|
| 277 |
const detail = await getRoom(roomId);
|
| 278 |
const messages: Message[] = detail.messages.map((m) => ({
|
| 279 |
id: m.id,
|
| 280 |
role: m.role,
|
| 281 |
content: m.content,
|
|
|
|
| 282 |
timestamp: new Date(m.created_at).getTime(),
|
| 283 |
sources: m.sources ?? [],
|
| 284 |
}));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
setChats((prev) =>
|
| 286 |
prev.map((chat) =>
|
| 287 |
-
chat.id === roomId
|
| 288 |
-
? { ...chat, messages, messagesLoaded: true }
|
| 289 |
-
: chat
|
| 290 |
)
|
| 291 |
);
|
| 292 |
-
|
|
|
|
|
|
|
| 293 |
setChats((prev) =>
|
| 294 |
prev.map((chat) =>
|
| 295 |
chat.id === roomId ? { ...chat, messagesLoaded: true } : chat
|
| 296 |
)
|
| 297 |
);
|
|
|
|
| 298 |
}
|
| 299 |
};
|
| 300 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
const currentChat = chats.find((chat) => chat.id === currentChatId);
|
| 302 |
|
| 303 |
-
const
|
| 304 |
-
setCurrentChatId(null);
|
| 305 |
-
};
|
| 306 |
|
| 307 |
-
const
|
| 308 |
if (!user) return;
|
| 309 |
try {
|
| 310 |
await deleteRoom(chatId, user.user_id);
|
| 311 |
} catch {
|
| 312 |
return;
|
| 313 |
}
|
| 314 |
-
const
|
| 315 |
-
setChats(
|
| 316 |
if (currentChatId === chatId) {
|
| 317 |
-
setCurrentChatId(
|
| 318 |
}
|
| 319 |
};
|
| 320 |
|
| 321 |
-
const deleteAllChats = async () => {
|
| 322 |
-
if (!user) return;
|
| 323 |
-
await Promise.allSettled(
|
| 324 |
-
chats.map((chat) => deleteRoom(chat.id, user.user_id))
|
| 325 |
-
);
|
| 326 |
-
setChats([]);
|
| 327 |
-
setCurrentChatId(null);
|
| 328 |
-
};
|
| 329 |
-
|
| 330 |
const handleLogout = () => {
|
| 331 |
localStorage.removeItem("chatbot_user");
|
| 332 |
navigate("/login");
|
| 333 |
};
|
| 334 |
|
| 335 |
-
const
|
| 336 |
-
|
|
|
|
| 337 |
|
| 338 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
|
|
|
|
|
|
|
| 340 |
if (!roomId) {
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
const newRoom: ChatRoom = {
|
| 344 |
-
id: res.data.id,
|
| 345 |
-
title: res.data.title,
|
| 346 |
-
messages: [],
|
| 347 |
-
createdAt: res.data.created_at,
|
| 348 |
-
updatedAt: res.data.updated_at,
|
| 349 |
-
messagesLoaded: true,
|
| 350 |
-
};
|
| 351 |
-
setChats((prev) => [newRoom, ...prev]);
|
| 352 |
-
roomId = newRoom.id;
|
| 353 |
-
setCurrentChatId(roomId);
|
| 354 |
-
} catch {
|
| 355 |
-
return;
|
| 356 |
-
}
|
| 357 |
}
|
| 358 |
|
| 359 |
const userMessage: Message = {
|
| 360 |
id: crypto.randomUUID(),
|
| 361 |
role: "user",
|
| 362 |
-
content:
|
| 363 |
timestamp: Date.now(),
|
| 364 |
};
|
| 365 |
|
|
@@ -375,10 +222,7 @@ export default function Main() {
|
|
| 375 |
)
|
| 376 |
);
|
| 377 |
|
| 378 |
-
const sentMessage = input;
|
| 379 |
-
setInput("");
|
| 380 |
setIsStreaming(true);
|
| 381 |
-
|
| 382 |
const assistantMsgId = crypto.randomUUID();
|
| 383 |
setStreamingMsgId(assistantMsgId);
|
| 384 |
|
|
@@ -394,7 +238,7 @@ export default function Main() {
|
|
| 394 |
role: "assistant",
|
| 395 |
content: "",
|
| 396 |
timestamp: Date.now(),
|
| 397 |
-
sources: [],
|
| 398 |
},
|
| 399 |
],
|
| 400 |
}
|
|
@@ -404,15 +248,68 @@ export default function Main() {
|
|
| 404 |
|
| 405 |
abortControllerRef.current = new AbortController();
|
| 406 |
|
| 407 |
-
|
| 408 |
-
const response = await streamChat(user.user_id, roomId, sentMessage);
|
| 409 |
|
|
|
|
|
|
|
| 410 |
if (!response.body) throw new Error("No response body");
|
| 411 |
|
| 412 |
const reader = response.body.getReader();
|
| 413 |
const decoder = new TextDecoder();
|
| 414 |
let buffer = "";
|
| 415 |
let currentEvent = "";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
|
| 417 |
while (true) {
|
| 418 |
const { done, value } = await reader.read();
|
|
@@ -423,63 +320,60 @@ export default function Main() {
|
|
| 423 |
buffer = lines.pop() ?? "";
|
| 424 |
|
| 425 |
for (const line of lines) {
|
| 426 |
-
if (line
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
currentEvent = line.replace("event:", "").trim();
|
| 428 |
} else if (line.startsWith("data:")) {
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
if (currentEvent === "sources" && data) {
|
| 432 |
-
const sources: ChatSource[] = JSON.parse(data);
|
| 433 |
-
setChats((prev) =>
|
| 434 |
-
prev.map((chat) =>
|
| 435 |
-
chat.id === roomId
|
| 436 |
-
? {
|
| 437 |
-
...chat,
|
| 438 |
-
messages: chat.messages.map((m) =>
|
| 439 |
-
m.id === assistantMsgId ? { ...m, sources } : m
|
| 440 |
-
),
|
| 441 |
-
}
|
| 442 |
-
: chat
|
| 443 |
-
)
|
| 444 |
-
);
|
| 445 |
-
} else if (currentEvent === "chunk" && data) {
|
| 446 |
-
setChats((prev) =>
|
| 447 |
-
prev.map((chat) =>
|
| 448 |
-
chat.id === roomId
|
| 449 |
-
? {
|
| 450 |
-
...chat,
|
| 451 |
-
messages: chat.messages.map((m) =>
|
| 452 |
-
m.id === assistantMsgId
|
| 453 |
-
? { ...m, content: m.content + data }
|
| 454 |
-
: m
|
| 455 |
-
),
|
| 456 |
-
}
|
| 457 |
-
: chat
|
| 458 |
-
)
|
| 459 |
-
);
|
| 460 |
-
} else if (currentEvent === "message" && data) {
|
| 461 |
-
setChats((prev) =>
|
| 462 |
-
prev.map((chat) =>
|
| 463 |
-
chat.id === roomId
|
| 464 |
-
? {
|
| 465 |
-
...chat,
|
| 466 |
-
messages: chat.messages.map((m) =>
|
| 467 |
-
m.id === assistantMsgId
|
| 468 |
-
? { ...m, content: data }
|
| 469 |
-
: m
|
| 470 |
-
),
|
| 471 |
-
}
|
| 472 |
-
: chat
|
| 473 |
-
)
|
| 474 |
-
);
|
| 475 |
-
} else if (currentEvent === "done") {
|
| 476 |
-
break;
|
| 477 |
-
}
|
| 478 |
}
|
| 479 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
}
|
|
|
|
| 481 |
} catch (err: unknown) {
|
| 482 |
if ((err as Error).name !== "AbortError") {
|
|
|
|
| 483 |
setChats((prev) =>
|
| 484 |
prev.map((chat) =>
|
| 485 |
chat.id === roomId
|
|
@@ -489,8 +383,7 @@ export default function Main() {
|
|
| 489 |
m.id === assistantMsgId
|
| 490 |
? {
|
| 491 |
...m,
|
| 492 |
-
content:
|
| 493 |
-
"Sorry, I couldn't get a response. Please try again.",
|
| 494 |
}
|
| 495 |
: m
|
| 496 |
),
|
|
@@ -498,275 +391,194 @@ export default function Main() {
|
|
| 498 |
: chat
|
| 499 |
)
|
| 500 |
);
|
|
|
|
| 501 |
}
|
| 502 |
} finally {
|
|
|
|
| 503 |
setIsStreaming(false);
|
| 504 |
setStreamingMsgId(null);
|
| 505 |
abortControllerRef.current = null;
|
| 506 |
}
|
| 507 |
-
};
|
| 508 |
|
| 509 |
-
const
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 513 |
}
|
| 514 |
-
};
|
| 515 |
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 533 |
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition group ${
|
| 548 |
-
currentChatId === chat.id
|
| 549 |
-
? "bg-white/25"
|
| 550 |
-
: "hover:bg-white/15"
|
| 551 |
-
}`}
|
| 552 |
-
>
|
| 553 |
-
<MessageSquare className="w-3.5 h-3.5 flex-shrink-0" />
|
| 554 |
-
<div
|
| 555 |
-
className="flex-1 truncate text-sm"
|
| 556 |
-
onClick={() => setCurrentChatId(chat.id)}
|
| 557 |
-
>
|
| 558 |
-
{chat.title}
|
| 559 |
-
</div>
|
| 560 |
-
<button
|
| 561 |
-
onClick={(e) => {
|
| 562 |
-
e.stopPropagation();
|
| 563 |
-
deleteChat(chat.id);
|
| 564 |
-
}}
|
| 565 |
-
className="opacity-0 group-hover:opacity-100 transition"
|
| 566 |
-
>
|
| 567 |
-
<Trash2 className="w-3.5 h-3.5 text-red-100 hover:text-white" />
|
| 568 |
-
</button>
|
| 569 |
-
</div>
|
| 570 |
-
))
|
| 571 |
-
)}
|
| 572 |
-
</div>
|
| 573 |
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 604 |
</div>
|
| 605 |
|
| 606 |
-
{/*
|
| 607 |
-
<div className="
|
| 608 |
-
{/*
|
| 609 |
-
<
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
>
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
</button>
|
| 630 |
-
</div>
|
| 631 |
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
<p className="text-sm text-slate-400">
|
| 642 |
-
Send a message to begin chatting
|
| 643 |
-
</p>
|
| 644 |
-
</div>
|
| 645 |
-
</div>
|
| 646 |
-
)}
|
| 647 |
-
|
| 648 |
-
{currentChat?.messages.map((message) => (
|
| 649 |
-
<div
|
| 650 |
-
key={message.id}
|
| 651 |
-
className={`flex ${
|
| 652 |
-
message.role === "user" ? "justify-end" : "justify-start"
|
| 653 |
-
}`}
|
| 654 |
-
>
|
| 655 |
-
{/* Avatar for assistant */}
|
| 656 |
-
{message.role === "assistant" && (
|
| 657 |
-
<div className="w-7 h-7 rounded-full bg-gradient-to-br from-[#059669] to-[#047857] flex items-center justify-center flex-shrink-0 mt-0.5 mr-2">
|
| 658 |
-
<Bot className="w-3.5 h-3.5 text-white" />
|
| 659 |
-
</div>
|
| 660 |
-
)}
|
| 661 |
-
|
| 662 |
-
<div
|
| 663 |
-
className={`max-w-2xl px-4 py-3 rounded-2xl ${
|
| 664 |
-
message.role === "user"
|
| 665 |
-
? "bg-[#3B82F6] text-white rounded-tr-sm shadow-sm"
|
| 666 |
-
: "bg-[#F3F4F6] border-0 text-slate-900 rounded-tl-sm shadow-sm"
|
| 667 |
-
}`}
|
| 668 |
-
>
|
| 669 |
-
{message.role === "user" ? (
|
| 670 |
-
<p className="text-sm whitespace-pre-wrap break-words leading-relaxed">
|
| 671 |
-
{message.content}
|
| 672 |
-
</p>
|
| 673 |
-
) : message.content === "" && streamingMsgId === message.id ? (
|
| 674 |
-
// Waiting for first chunk — show typing indicator
|
| 675 |
-
<TypingIndicator />
|
| 676 |
-
) : (
|
| 677 |
-
// Render markdown for assistant messages
|
| 678 |
-
<div className="text-slate-900">
|
| 679 |
-
<ReactMarkdown
|
| 680 |
-
remarkPlugins={[remarkGfm, remarkMath]}
|
| 681 |
-
rehypePlugins={[rehypeKatex]}
|
| 682 |
-
components={markdownComponents}
|
| 683 |
-
>
|
| 684 |
-
{preprocessMarkdown(message.content)}
|
| 685 |
-
</ReactMarkdown>
|
| 686 |
-
</div>
|
| 687 |
-
)}
|
| 688 |
-
|
| 689 |
-
{/* Sources */}
|
| 690 |
-
{message.role === "assistant" &&
|
| 691 |
-
message.sources &&
|
| 692 |
-
message.sources.length > 0 && (
|
| 693 |
-
<div className="mt-2 pt-2 border-t border-slate-100">
|
| 694 |
-
<p className="text-[10px] text-slate-400 mb-1.5">
|
| 695 |
-
Sources:
|
| 696 |
-
</p>
|
| 697 |
-
<div className="flex flex-wrap gap-1">
|
| 698 |
-
{message.sources.map((src, i) => (
|
| 699 |
-
<span
|
| 700 |
-
key={i}
|
| 701 |
-
className="text-[10px] bg-slate-100 text-slate-600 px-2 py-0.5 rounded-full border border-slate-200"
|
| 702 |
-
title={
|
| 703 |
-
src.page_label
|
| 704 |
-
? `Page ${src.page_label}`
|
| 705 |
-
: undefined
|
| 706 |
-
}
|
| 707 |
-
>
|
| 708 |
-
📄 {src.filename}
|
| 709 |
-
{src.page_label ? ` p.${src.page_label}` : ""}
|
| 710 |
-
</span>
|
| 711 |
-
))}
|
| 712 |
-
</div>
|
| 713 |
-
</div>
|
| 714 |
-
)}
|
| 715 |
-
</div>
|
| 716 |
-
</div>
|
| 717 |
-
))}
|
| 718 |
-
|
| 719 |
-
{!currentChat && chats.length === 0 && !roomsLoading && (
|
| 720 |
-
<div className="flex items-center justify-center h-full">
|
| 721 |
-
<div className="text-center">
|
| 722 |
-
<MessageSquare className="w-12 h-12 text-slate-300 mx-auto mb-3" />
|
| 723 |
-
<h2 className="text-base text-slate-600 mb-1">
|
| 724 |
-
Welcome to Chatbot
|
| 725 |
-
</h2>
|
| 726 |
-
<p className="text-sm text-slate-400">
|
| 727 |
-
Create a new chat to get started
|
| 728 |
-
</p>
|
| 729 |
-
</div>
|
| 730 |
-
</div>
|
| 731 |
-
)}
|
| 732 |
-
|
| 733 |
-
<div ref={messagesEndRef} />
|
| 734 |
-
</div>
|
| 735 |
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
disabled={!input.trim() || isStreaming}
|
| 752 |
-
className="bg-[#3B82F6] hover:bg-[#2563EB] text-white p-2.5 rounded-lg transition-all duration-200 hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed flex-shrink-0"
|
| 753 |
-
>
|
| 754 |
-
{isStreaming ? (
|
| 755 |
-
<Loader2 className="w-4 h-4 animate-spin" />
|
| 756 |
-
) : (
|
| 757 |
-
<Send className="w-4 h-4" />
|
| 758 |
-
)}
|
| 759 |
-
</button>
|
| 760 |
-
</div>
|
| 761 |
-
</div>
|
| 762 |
</div>
|
| 763 |
</div>
|
| 764 |
|
| 765 |
-
{/* Knowledge Management Modal */}
|
| 766 |
<KnowledgeManagement
|
| 767 |
open={knowledgeOpen}
|
| 768 |
onClose={() => setKnowledgeOpen(false)}
|
| 769 |
/>
|
| 770 |
-
</
|
| 771 |
);
|
| 772 |
}
|
|
|
|
| 1 |
+
import { useState, useEffect, useRef, useCallback } from "react";
|
| 2 |
import { useNavigate } from "react-router";
|
| 3 |
+
import { Database, Menu } from "lucide-react";
|
| 4 |
+
import { AnimatePresence } from "motion/react";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
import KnowledgeManagement from "./KnowledgeManagement";
|
| 6 |
import {
|
| 7 |
getRooms,
|
|
|
|
| 11 |
streamChat,
|
| 12 |
type ChatSource,
|
| 13 |
} from "../../services/api";
|
| 14 |
+
import { textToSpeech } from "../../services/voiceApi";
|
| 15 |
+
import { replayAudio } from "../../audio/AudioPlayer";
|
| 16 |
+
import ChatLayout from "./chat/ChatLayout";
|
| 17 |
+
import Sidebar from "./chat/Sidebar";
|
| 18 |
+
import ChatWindow from "./chat/ChatWindow";
|
| 19 |
+
import ChatInput from "./chat/ChatInput";
|
| 20 |
+
import VoiceStatusBar from "./chat/VoiceStatusBar";
|
| 21 |
+
import { useVoiceSession } from "../../hooks/useVoiceSession";
|
| 22 |
+
import type { VoiceState } from "../../hooks/useVoiceSession";
|
| 23 |
+
import type { Message, ChatSession, StoredUser } from "./chat/types";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
interface ChatRoom {
|
| 26 |
id: string;
|
|
|
|
| 31 |
messagesLoaded: boolean;
|
| 32 |
}
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
export default function Main() {
|
| 35 |
const navigate = useNavigate();
|
|
|
|
| 36 |
const [chats, setChats] = useState<ChatRoom[]>([]);
|
| 37 |
const [currentChatId, setCurrentChatId] = useState<string | null>(null);
|
|
|
|
| 38 |
const [isStreaming, setIsStreaming] = useState(false);
|
| 39 |
const [streamingMsgId, setStreamingMsgId] = useState<string | null>(null);
|
| 40 |
const [roomsLoading, setRoomsLoading] = useState(false);
|
|
|
|
|
|
|
| 41 |
const [user, setUser] = useState<StoredUser | null>(null);
|
| 42 |
const [knowledgeOpen, setKnowledgeOpen] = useState(false);
|
| 43 |
+
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
| 44 |
const abortControllerRef = useRef<AbortController | null>(null);
|
| 45 |
+
const isVoiceActiveRef = useRef(false);
|
| 46 |
+
const setVoiceStateRef = useRef<((s: VoiceState) => void) | null>(null);
|
| 47 |
+
|
| 48 |
+
// Stable refs so voice callbacks always see the latest values
|
| 49 |
+
const currentChatIdRef = useRef<string | null>(null);
|
| 50 |
+
useEffect(() => { currentChatIdRef.current = currentChatId; }, [currentChatId]);
|
| 51 |
+
|
| 52 |
+
const userRef = useRef<StoredUser | null>(null);
|
| 53 |
+
useEffect(() => { userRef.current = user; }, [user]);
|
| 54 |
+
|
| 55 |
+
useEffect(() => {
|
| 56 |
+
if (currentChatId) {
|
| 57 |
+
localStorage.setItem("chatbot_last_room_id", currentChatId);
|
| 58 |
+
}
|
| 59 |
+
}, [currentChatId]);
|
| 60 |
|
| 61 |
useEffect(() => {
|
| 62 |
const storedUser = localStorage.getItem("chatbot_user");
|
|
|
|
| 67 |
}
|
| 68 |
}, []);
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
useEffect(() => {
|
| 71 |
if (!currentChatId) return;
|
| 72 |
const chat = chats.find((c) => c.id === currentChatId);
|
|
|
|
| 78 |
|
| 79 |
const loadRooms = async (userId: string) => {
|
| 80 |
setRoomsLoading(true);
|
|
|
|
| 81 |
try {
|
| 82 |
const apiRooms = await getRooms(userId);
|
| 83 |
const mapped: ChatRoom[] = apiRooms.map((r) => ({
|
|
|
|
| 90 |
}));
|
| 91 |
setChats(mapped);
|
| 92 |
if (mapped.length > 0) {
|
| 93 |
+
const lastRoomId = localStorage.getItem("chatbot_last_room_id");
|
| 94 |
+
const restoredId = lastRoomId && mapped.find((r) => r.id === lastRoomId)
|
| 95 |
+
? lastRoomId
|
| 96 |
+
: mapped[0].id;
|
| 97 |
+
setCurrentChatId(restoredId);
|
| 98 |
}
|
| 99 |
+
} catch {
|
| 100 |
+
// silently fail — UI shows empty state
|
|
|
|
|
|
|
| 101 |
} finally {
|
| 102 |
setRoomsLoading(false);
|
| 103 |
}
|
| 104 |
};
|
| 105 |
|
| 106 |
+
const loadRoomMessages = async (roomId: string): Promise<Message[]> => {
|
| 107 |
try {
|
| 108 |
const detail = await getRoom(roomId);
|
| 109 |
const messages: Message[] = detail.messages.map((m) => ({
|
| 110 |
id: m.id,
|
| 111 |
role: m.role,
|
| 112 |
content: m.content,
|
| 113 |
+
audioText: m.audio_text ?? undefined,
|
| 114 |
timestamp: new Date(m.created_at).getTime(),
|
| 115 |
sources: m.sources ?? [],
|
| 116 |
}));
|
| 117 |
+
const asstMsgs = messages.filter(m => m.role === "assistant");
|
| 118 |
+
const lastAsst = asstMsgs[asstMsgs.length - 1];
|
| 119 |
+
if (lastAsst) {
|
| 120 |
+
const hasCR = lastAsst.content.includes("\r");
|
| 121 |
+
// console.log("[loadRoomMessages] last assistant contentLen=", lastAsst.content.length, "hasCR=", hasCR);
|
| 122 |
+
// console.log("[loadRoomMessages] CONTENT JSON →", JSON.stringify(lastAsst.content.slice(0, 600)));
|
| 123 |
+
}
|
| 124 |
+
// console.log("[loadRoomMessages] room", roomId, "→", messages.length, "messages from server");
|
| 125 |
setChats((prev) =>
|
| 126 |
prev.map((chat) =>
|
| 127 |
+
chat.id === roomId ? { ...chat, messages, messagesLoaded: true } : chat
|
|
|
|
|
|
|
| 128 |
)
|
| 129 |
);
|
| 130 |
+
return messages;
|
| 131 |
+
} catch (err) {
|
| 132 |
+
console.error("[loadRoomMessages] failed:", err);
|
| 133 |
setChats((prev) =>
|
| 134 |
prev.map((chat) =>
|
| 135 |
chat.id === roomId ? { ...chat, messagesLoaded: true } : chat
|
| 136 |
)
|
| 137 |
);
|
| 138 |
+
return [];
|
| 139 |
}
|
| 140 |
};
|
| 141 |
|
| 142 |
+
// Ensures a chat room exists; creates one for voice if needed.
|
| 143 |
+
const ensureRoom = useCallback(async (title?: string): Promise<string | null> => {
|
| 144 |
+
if (currentChatIdRef.current) return currentChatIdRef.current;
|
| 145 |
+
const currentUser = userRef.current;
|
| 146 |
+
if (!currentUser) return null;
|
| 147 |
+
try {
|
| 148 |
+
const res = await createRoom(currentUser.user_id, title ?? "Voice Session");
|
| 149 |
+
const newRoom: ChatRoom = {
|
| 150 |
+
id: res.data.id,
|
| 151 |
+
title: res.data.title,
|
| 152 |
+
messages: [],
|
| 153 |
+
createdAt: res.data.created_at,
|
| 154 |
+
updatedAt: res.data.updated_at,
|
| 155 |
+
messagesLoaded: true,
|
| 156 |
+
};
|
| 157 |
+
setChats((prev) => [newRoom, ...prev]);
|
| 158 |
+
setCurrentChatId(newRoom.id);
|
| 159 |
+
return newRoom.id;
|
| 160 |
+
} catch {
|
| 161 |
+
return null;
|
| 162 |
+
}
|
| 163 |
+
}, []);
|
| 164 |
+
|
| 165 |
const currentChat = chats.find((chat) => chat.id === currentChatId);
|
| 166 |
|
| 167 |
+
const handleNewChat = () => setCurrentChatId(null);
|
|
|
|
|
|
|
| 168 |
|
| 169 |
+
const handleDeleteSession = async (chatId: string) => {
|
| 170 |
if (!user) return;
|
| 171 |
try {
|
| 172 |
await deleteRoom(chatId, user.user_id);
|
| 173 |
} catch {
|
| 174 |
return;
|
| 175 |
}
|
| 176 |
+
const updated = chats.filter((c) => c.id !== chatId);
|
| 177 |
+
setChats(updated);
|
| 178 |
if (currentChatId === chatId) {
|
| 179 |
+
setCurrentChatId(updated.length > 0 ? updated[0].id : null);
|
| 180 |
}
|
| 181 |
};
|
| 182 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
const handleLogout = () => {
|
| 184 |
localStorage.removeItem("chatbot_user");
|
| 185 |
navigate("/login");
|
| 186 |
};
|
| 187 |
|
| 188 |
+
const handleStop = () => {
|
| 189 |
+
abortControllerRef.current?.abort();
|
| 190 |
+
};
|
| 191 |
|
| 192 |
+
const handleSend = useCallback(async (text: string, skipReload = false) => {
|
| 193 |
+
// console.log("[handleSend] called, user:", user?.user_id ?? "null", "text:", text.slice(0, 40));
|
| 194 |
+
if (!user) {
|
| 195 |
+
console.warn("[handleSend] early return: no user");
|
| 196 |
+
return;
|
| 197 |
+
}
|
| 198 |
|
| 199 |
+
let roomId = await ensureRoom(text.slice(0, 50));
|
| 200 |
+
// console.log("[handleSend] roomId:", roomId);
|
| 201 |
if (!roomId) {
|
| 202 |
+
// console.warn("[handleSend] early return: no roomId");
|
| 203 |
+
return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
}
|
| 205 |
|
| 206 |
const userMessage: Message = {
|
| 207 |
id: crypto.randomUUID(),
|
| 208 |
role: "user",
|
| 209 |
+
content: text,
|
| 210 |
timestamp: Date.now(),
|
| 211 |
};
|
| 212 |
|
|
|
|
| 222 |
)
|
| 223 |
);
|
| 224 |
|
|
|
|
|
|
|
| 225 |
setIsStreaming(true);
|
|
|
|
| 226 |
const assistantMsgId = crypto.randomUUID();
|
| 227 |
setStreamingMsgId(assistantMsgId);
|
| 228 |
|
|
|
|
| 238 |
role: "assistant",
|
| 239 |
content: "",
|
| 240 |
timestamp: Date.now(),
|
| 241 |
+
sources: [] as ChatSource[],
|
| 242 |
},
|
| 243 |
],
|
| 244 |
}
|
|
|
|
| 248 |
|
| 249 |
abortControllerRef.current = new AbortController();
|
| 250 |
|
| 251 |
+
let audioText = "";
|
|
|
|
| 252 |
|
| 253 |
+
try {
|
| 254 |
+
const response = await streamChat(user.user_id, roomId, text);
|
| 255 |
if (!response.body) throw new Error("No response body");
|
| 256 |
|
| 257 |
const reader = response.body.getReader();
|
| 258 |
const decoder = new TextDecoder();
|
| 259 |
let buffer = "";
|
| 260 |
let currentEvent = "";
|
| 261 |
+
let currentDataLines: string[] = [];
|
| 262 |
+
let streamDone = false;
|
| 263 |
+
|
| 264 |
+
const dispatchEvent = (eventType: string, data: string) => {
|
| 265 |
+
if (eventType === "sources" && data) {
|
| 266 |
+
const sources: ChatSource[] = JSON.parse(data);
|
| 267 |
+
setChats((prev) =>
|
| 268 |
+
prev.map((chat) =>
|
| 269 |
+
chat.id === roomId
|
| 270 |
+
? {
|
| 271 |
+
...chat,
|
| 272 |
+
messages: chat.messages.map((m) =>
|
| 273 |
+
m.id === assistantMsgId ? { ...m, sources } : m
|
| 274 |
+
),
|
| 275 |
+
}
|
| 276 |
+
: chat
|
| 277 |
+
)
|
| 278 |
+
);
|
| 279 |
+
} else if (eventType === "chunk") {
|
| 280 |
+
setChats((prev) =>
|
| 281 |
+
prev.map((chat) =>
|
| 282 |
+
chat.id === roomId
|
| 283 |
+
? {
|
| 284 |
+
...chat,
|
| 285 |
+
messages: chat.messages.map((m) =>
|
| 286 |
+
m.id === assistantMsgId
|
| 287 |
+
? { ...m, content: m.content + data }
|
| 288 |
+
: m
|
| 289 |
+
),
|
| 290 |
+
}
|
| 291 |
+
: chat
|
| 292 |
+
)
|
| 293 |
+
);
|
| 294 |
+
} else if (eventType === "message" && data) {
|
| 295 |
+
setChats((prev) =>
|
| 296 |
+
prev.map((chat) =>
|
| 297 |
+
chat.id === roomId
|
| 298 |
+
? {
|
| 299 |
+
...chat,
|
| 300 |
+
messages: chat.messages.map((m) =>
|
| 301 |
+
m.id === assistantMsgId ? { ...m, content: data } : m
|
| 302 |
+
),
|
| 303 |
+
}
|
| 304 |
+
: chat
|
| 305 |
+
)
|
| 306 |
+
);
|
| 307 |
+
} else if (eventType === "audio_text" && data) {
|
| 308 |
+
audioText = data;
|
| 309 |
+
} else if (eventType === "done") {
|
| 310 |
+
streamDone = true;
|
| 311 |
+
}
|
| 312 |
+
};
|
| 313 |
|
| 314 |
while (true) {
|
| 315 |
const { done, value } = await reader.read();
|
|
|
|
| 320 |
buffer = lines.pop() ?? "";
|
| 321 |
|
| 322 |
for (const line of lines) {
|
| 323 |
+
if (line === "") {
|
| 324 |
+
// Blank line = end of SSE event — dispatch with all accumulated data lines joined by \n
|
| 325 |
+
if (currentEvent) {
|
| 326 |
+
dispatchEvent(currentEvent, currentDataLines.join("\n"));
|
| 327 |
+
}
|
| 328 |
+
currentEvent = "";
|
| 329 |
+
currentDataLines = [];
|
| 330 |
+
} else if (line.startsWith("event:")) {
|
| 331 |
currentEvent = line.replace("event:", "").trim();
|
| 332 |
} else if (line.startsWith("data:")) {
|
| 333 |
+
currentDataLines.push(line.replace(/^data: ?/, ""));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
}
|
| 335 |
}
|
| 336 |
+
if (streamDone) break;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
if (skipReload) {
|
| 340 |
+
// Voice mode: skip GET /room, return immediately so TTS can start without delay.
|
| 341 |
+
// audioText and audioChunks will be stored on the temp assistantMsgId.
|
| 342 |
+
if (audioText) {
|
| 343 |
+
setChats((prev) =>
|
| 344 |
+
prev.map((chat) =>
|
| 345 |
+
chat.id === roomId
|
| 346 |
+
? {
|
| 347 |
+
...chat,
|
| 348 |
+
messages: chat.messages.map((m) =>
|
| 349 |
+
m.id === assistantMsgId ? { ...m, audioText } : m
|
| 350 |
+
),
|
| 351 |
+
}
|
| 352 |
+
: chat
|
| 353 |
+
)
|
| 354 |
+
);
|
| 355 |
+
}
|
| 356 |
+
return { audioText, assistantMsgId };
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
if (audioText) {
|
| 360 |
+
setChats((prev) =>
|
| 361 |
+
prev.map((chat) =>
|
| 362 |
+
chat.id === roomId
|
| 363 |
+
? {
|
| 364 |
+
...chat,
|
| 365 |
+
messages: chat.messages.map((m) =>
|
| 366 |
+
m.id === assistantMsgId ? { ...m, audioText } : m
|
| 367 |
+
),
|
| 368 |
+
}
|
| 369 |
+
: chat
|
| 370 |
+
)
|
| 371 |
+
);
|
| 372 |
}
|
| 373 |
+
return { audioText, assistantMsgId };
|
| 374 |
} catch (err: unknown) {
|
| 375 |
if ((err as Error).name !== "AbortError") {
|
| 376 |
+
console.error("[handleSend] streamChat error:", err);
|
| 377 |
setChats((prev) =>
|
| 378 |
prev.map((chat) =>
|
| 379 |
chat.id === roomId
|
|
|
|
| 383 |
m.id === assistantMsgId
|
| 384 |
? {
|
| 385 |
...m,
|
| 386 |
+
content: "Sorry, I couldn't get a response. Please try again.",
|
|
|
|
| 387 |
}
|
| 388 |
: m
|
| 389 |
),
|
|
|
|
| 391 |
: chat
|
| 392 |
)
|
| 393 |
);
|
| 394 |
+
audioText = "";
|
| 395 |
}
|
| 396 |
} finally {
|
| 397 |
+
// console.log("[handleSend] finally: clearing streamingMsgId →", assistantMsgId, "| skipReload:", skipReload);
|
| 398 |
setIsStreaming(false);
|
| 399 |
setStreamingMsgId(null);
|
| 400 |
abortControllerRef.current = null;
|
| 401 |
}
|
| 402 |
+
}, [user, ensureRoom]);
|
| 403 |
|
| 404 |
+
const cancelTtsRef = useRef<(() => void) | null>(null);
|
| 405 |
+
|
| 406 |
+
const playTtsAudio = useCallback(async (
|
| 407 |
+
ttsText: string,
|
| 408 |
+
onStarted?: () => void,
|
| 409 |
+
): Promise<{ chunks: ArrayBuffer[]; sampleRate: number }> => {
|
| 410 |
+
// Stop any in-flight TTS before starting a new one (e.g. rapid re-query).
|
| 411 |
+
cancelTtsRef.current?.();
|
| 412 |
+
cancelTtsRef.current = null;
|
| 413 |
+
try {
|
| 414 |
+
const { pcm, sampleRate } = await textToSpeech(ttsText);
|
| 415 |
+
const durationMs = (pcm.byteLength / 2 / sampleRate) * 1000;
|
| 416 |
+
// Use the same proven playback path as the speaker button: schedule the
|
| 417 |
+
// full response at once via replayAudio, avoiding all streaming complexity.
|
| 418 |
+
const cancel = replayAudio([pcm], sampleRate);
|
| 419 |
+
cancelTtsRef.current = cancel;
|
| 420 |
+
onStarted?.();
|
| 421 |
+
await new Promise<void>((resolve) => setTimeout(resolve, durationMs + 150));
|
| 422 |
+
cancel();
|
| 423 |
+
cancelTtsRef.current = null;
|
| 424 |
+
return { chunks: [pcm], sampleRate };
|
| 425 |
+
} catch {
|
| 426 |
+
// TTS failure is non-fatal
|
| 427 |
+
return { chunks: [], sampleRate: 24000 };
|
| 428 |
}
|
| 429 |
+
}, []);
|
| 430 |
|
| 431 |
+
const { voiceState, start, stop, stopRecording, setStateExternal, isActive: isVoiceActive } = useVoiceSession({
|
| 432 |
+
onTranscript: async (text: string) => {
|
| 433 |
+
// console.log("[onTranscript] received:", text);
|
| 434 |
+
// Pass skipReload=true so handleSend returns immediately after streaming
|
| 435 |
+
// without waiting for GET /room — TTS starts with zero extra delay.
|
| 436 |
+
const result = await handleSend(text, true);
|
| 437 |
+
const { audioText = "", assistantMsgId } = result ?? {};
|
| 438 |
+
// console.log("[onTranscript] handleSend done, audioText:", audioText ? audioText.slice(0, 40) : "(empty)");
|
| 439 |
+
if (audioText && isVoiceActiveRef.current) {
|
| 440 |
+
const { chunks, sampleRate } = await playTtsAudio(audioText, () => {
|
| 441 |
+
if (isVoiceActiveRef.current) setVoiceStateRef.current?.("SPEAKING");
|
| 442 |
+
});
|
| 443 |
+
if (assistantMsgId && chunks.length > 0) {
|
| 444 |
+
setChats((prev) =>
|
| 445 |
+
prev.map((chat) =>
|
| 446 |
+
chat.id === currentChatIdRef.current
|
| 447 |
+
? {
|
| 448 |
+
...chat,
|
| 449 |
+
messages: chat.messages.map((m) =>
|
| 450 |
+
m.id === assistantMsgId
|
| 451 |
+
? { ...m, audioChunks: chunks, audioSampleRate: sampleRate }
|
| 452 |
+
: m
|
| 453 |
+
),
|
| 454 |
+
}
|
| 455 |
+
: chat
|
| 456 |
+
)
|
| 457 |
+
);
|
| 458 |
+
}
|
| 459 |
+
}
|
| 460 |
+
if (isVoiceActiveRef.current) setVoiceStateRef.current?.("IDLE");
|
| 461 |
+
},
|
| 462 |
+
sessionParams: {},
|
| 463 |
+
});
|
| 464 |
|
| 465 |
+
// Keep refs in sync with latest values
|
| 466 |
+
useEffect(() => { isVoiceActiveRef.current = isVoiceActive; }, [isVoiceActive]);
|
| 467 |
+
useEffect(() => { setVoiceStateRef.current = setStateExternal; }, [setStateExternal]);
|
| 468 |
+
|
| 469 |
+
const handleVoiceToggle = useCallback(() => {
|
| 470 |
+
if (!isVoiceActive) {
|
| 471 |
+
start();
|
| 472 |
+
} else if (voiceState === "LISTENING") {
|
| 473 |
+
stopRecording();
|
| 474 |
+
} else {
|
| 475 |
+
stop();
|
| 476 |
+
}
|
| 477 |
+
}, [isVoiceActive, voiceState, start, stop, stopRecording]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
|
| 479 |
+
const sessions: ChatSession[] = chats.map((c) => ({
|
| 480 |
+
id: c.id,
|
| 481 |
+
title: c.title,
|
| 482 |
+
createdAt: c.createdAt,
|
| 483 |
+
updatedAt: c.updatedAt,
|
| 484 |
+
}));
|
| 485 |
+
|
| 486 |
+
return (
|
| 487 |
+
<ChatLayout
|
| 488 |
+
sidebar={
|
| 489 |
+
<Sidebar
|
| 490 |
+
sessions={sessions}
|
| 491 |
+
activeSessionId={currentChatId}
|
| 492 |
+
isLoadingSessions={roomsLoading}
|
| 493 |
+
user={user}
|
| 494 |
+
onNewChat={handleNewChat}
|
| 495 |
+
onSelectSession={setCurrentChatId}
|
| 496 |
+
onDeleteSession={handleDeleteSession}
|
| 497 |
+
onLogout={handleLogout}
|
| 498 |
+
mobileOpen={mobileSidebarOpen}
|
| 499 |
+
onMobileClose={() => setMobileSidebarOpen(false)}
|
| 500 |
+
/>
|
| 501 |
+
}
|
| 502 |
+
>
|
| 503 |
+
{/* Background decoration */}
|
| 504 |
+
<div className="absolute inset-0 pointer-events-none z-0 overflow-hidden">
|
| 505 |
+
<div
|
| 506 |
+
className="absolute inset-0 opacity-[0.13]"
|
| 507 |
+
style={{
|
| 508 |
+
backgroundImage: "radial-gradient(circle, #94a3b8 1px, transparent 1px)",
|
| 509 |
+
backgroundSize: "28px 28px",
|
| 510 |
+
}}
|
| 511 |
+
/>
|
| 512 |
+
<div
|
| 513 |
+
className="absolute -top-28 -right-28 w-80 h-80 rounded-full blur-3xl opacity-[0.08]"
|
| 514 |
+
style={{ background: "#0ea5e9" }}
|
| 515 |
+
/>
|
| 516 |
+
<div
|
| 517 |
+
className="absolute -bottom-28 -left-28 w-96 h-96 rounded-full blur-3xl opacity-[0.07]"
|
| 518 |
+
style={{ background: "#f97316" }}
|
| 519 |
+
/>
|
| 520 |
+
<div
|
| 521 |
+
className="absolute top-1/2 -left-32 w-64 h-64 rounded-full blur-3xl opacity-[0.05]"
|
| 522 |
+
style={{ background: "#10b981" }}
|
| 523 |
+
/>
|
| 524 |
</div>
|
| 525 |
|
| 526 |
+
{/* Header */}
|
| 527 |
+
<div className="relative z-10 bg-white border-b border-neutral-100 px-4 py-3 flex items-center gap-3">
|
| 528 |
+
{/* Hamburger — mobile only */}
|
| 529 |
+
<button
|
| 530 |
+
className="md:hidden flex-shrink-0 p-1.5 rounded-lg text-neutral-500 hover:text-brand-green hover:bg-brand-green-50 transition-all"
|
| 531 |
+
onClick={() => setMobileSidebarOpen(true)}
|
| 532 |
+
aria-label="Open menu"
|
| 533 |
+
>
|
| 534 |
+
<Menu className="w-5 h-5" />
|
| 535 |
+
</button>
|
| 536 |
+
|
| 537 |
+
<h1 className="text-base font-bold text-neutral-900 flex-1 truncate">
|
| 538 |
+
{currentChat?.title ?? "New Chat"}
|
| 539 |
+
</h1>
|
| 540 |
+
|
| 541 |
+
<button
|
| 542 |
+
onClick={() => setKnowledgeOpen(true)}
|
| 543 |
+
className="flex items-center gap-2 bg-brand-amber text-white px-3 py-2 rounded-lg text-sm font-semibold hover:brightness-105 hover:scale-105 transition-all duration-200 flex-shrink-0"
|
| 544 |
+
>
|
| 545 |
+
<Database className="w-4 h-4" />
|
| 546 |
+
<span className="hidden sm:inline">Knowledge</span>
|
| 547 |
+
</button>
|
| 548 |
+
</div>
|
|
|
|
|
|
|
| 549 |
|
| 550 |
+
{/* Chat window */}
|
| 551 |
+
<div className="relative z-10 flex-1 flex flex-col min-h-0">
|
| 552 |
+
<ChatWindow
|
| 553 |
+
messages={currentChat?.messages ?? []}
|
| 554 |
+
isLoading={isStreaming}
|
| 555 |
+
streamingMsgId={streamingMsgId}
|
| 556 |
+
userName={user?.name}
|
| 557 |
+
/>
|
| 558 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 559 |
|
| 560 |
+
{/* Input area */}
|
| 561 |
+
<div className="relative z-10 border-t border-neutral-100 bg-white/80 backdrop-blur-sm">
|
| 562 |
+
<div className="w-full max-w-4xl xl:max-w-5xl mx-auto flex flex-col gap-2 px-3 sm:px-4 pt-3 pb-4">
|
| 563 |
+
<AnimatePresence>
|
| 564 |
+
{isVoiceActive && (
|
| 565 |
+
<VoiceStatusBar voiceState={voiceState} onStop={stop} />
|
| 566 |
+
)}
|
| 567 |
+
</AnimatePresence>
|
| 568 |
+
<ChatInput
|
| 569 |
+
onSend={handleSend}
|
| 570 |
+
onStop={handleStop}
|
| 571 |
+
isLoading={isStreaming}
|
| 572 |
+
voiceState={voiceState}
|
| 573 |
+
onVoiceToggle={handleVoiceToggle}
|
| 574 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 575 |
</div>
|
| 576 |
</div>
|
| 577 |
|
|
|
|
| 578 |
<KnowledgeManagement
|
| 579 |
open={knowledgeOpen}
|
| 580 |
onClose={() => setKnowledgeOpen(false)}
|
| 581 |
/>
|
| 582 |
+
</ChatLayout>
|
| 583 |
);
|
| 584 |
}
|
src/app/components/chat/ChatInput.tsx
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useRef, useEffect } from "react";
|
| 2 |
+
import { SendHorizontal, Square } from "lucide-react";
|
| 3 |
+
import type { VoiceState } from "../../../hooks/useVoiceSession";
|
| 4 |
+
import VoiceMicButton from "./VoiceMicButton";
|
| 5 |
+
|
| 6 |
+
interface ChatInputProps {
|
| 7 |
+
onSend: (text: string) => void;
|
| 8 |
+
onStop?: () => void;
|
| 9 |
+
isLoading: boolean;
|
| 10 |
+
disabled?: boolean;
|
| 11 |
+
voiceState: VoiceState;
|
| 12 |
+
onVoiceToggle: () => void;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export default function ChatInput({
|
| 16 |
+
onSend,
|
| 17 |
+
onStop,
|
| 18 |
+
isLoading,
|
| 19 |
+
disabled,
|
| 20 |
+
voiceState,
|
| 21 |
+
onVoiceToggle,
|
| 22 |
+
}: ChatInputProps) {
|
| 23 |
+
const [value, setValue] = useState("");
|
| 24 |
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
| 25 |
+
|
| 26 |
+
const isVoiceActive = voiceState !== "IDLE" && voiceState !== "ERROR";
|
| 27 |
+
|
| 28 |
+
useEffect(() => {
|
| 29 |
+
const el = textareaRef.current;
|
| 30 |
+
if (!el) return;
|
| 31 |
+
el.style.height = "auto";
|
| 32 |
+
el.style.height = Math.min(el.scrollHeight, 50) + "px";
|
| 33 |
+
}, [value]);
|
| 34 |
+
|
| 35 |
+
const handleSend = () => {
|
| 36 |
+
const trimmed = value.trim();
|
| 37 |
+
if (!trimmed || isLoading || disabled || isVoiceActive) return;
|
| 38 |
+
onSend(trimmed);
|
| 39 |
+
setValue("");
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
| 43 |
+
if (e.key === "Enter" && !e.shiftKey) {
|
| 44 |
+
e.preventDefault();
|
| 45 |
+
handleSend();
|
| 46 |
+
}
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
const canSend = value.trim().length > 0 && !disabled && !isVoiceActive;
|
| 50 |
+
|
| 51 |
+
return (
|
| 52 |
+
<div
|
| 53 |
+
className={`bg-white border rounded-2xl shadow-sm px-4 py-3 transition-all ${
|
| 54 |
+
disabled || isVoiceActive
|
| 55 |
+
? "opacity-80 border-neutral-200"
|
| 56 |
+
: "border-neutral-200 focus-within:border-brand-green focus-within:shadow-md focus-within:shadow-brand-green/10"
|
| 57 |
+
}`}
|
| 58 |
+
>
|
| 59 |
+
<div className="flex items-end gap-3">
|
| 60 |
+
<VoiceMicButton
|
| 61 |
+
voiceState={voiceState}
|
| 62 |
+
onToggle={onVoiceToggle}
|
| 63 |
+
disabled={isLoading}
|
| 64 |
+
/>
|
| 65 |
+
|
| 66 |
+
<textarea
|
| 67 |
+
ref={textareaRef}
|
| 68 |
+
value={value}
|
| 69 |
+
onChange={(e) => setValue(e.target.value)}
|
| 70 |
+
onKeyDown={handleKeyDown}
|
| 71 |
+
placeholder={
|
| 72 |
+
isVoiceActive
|
| 73 |
+
? "Voice session active — speak to interact"
|
| 74 |
+
: "Ask me anything… (Enter to send, Shift+Enter for newline)"
|
| 75 |
+
}
|
| 76 |
+
disabled={isLoading || disabled || isVoiceActive}
|
| 77 |
+
rows={1}
|
| 78 |
+
className="flex-1 resize-none bg-transparent text-sm text-neutral-900 leading-6 placeholder:text-neutral-400 outline-none overflow-y-auto"
|
| 79 |
+
style={{ minHeight: "24px", maxHeight: "50px" }}
|
| 80 |
+
/>
|
| 81 |
+
|
| 82 |
+
{isLoading ? (
|
| 83 |
+
<button
|
| 84 |
+
onClick={onStop}
|
| 85 |
+
className="w-9 h-9 flex-shrink-0 rounded-xl bg-gradient-to-br from-brand-green-light to-brand-green text-white shadow-md shadow-brand-green/25 hover:brightness-105 flex items-center justify-center transition-all"
|
| 86 |
+
aria-label="Stop"
|
| 87 |
+
>
|
| 88 |
+
<Square className="h-3.5 w-3.5" />
|
| 89 |
+
</button>
|
| 90 |
+
) : (
|
| 91 |
+
<button
|
| 92 |
+
onClick={handleSend}
|
| 93 |
+
disabled={!canSend}
|
| 94 |
+
className={`w-9 h-9 flex-shrink-0 rounded-xl flex items-center justify-center transition-all ${
|
| 95 |
+
canSend
|
| 96 |
+
? "bg-gradient-to-br from-brand-green-light to-brand-green text-white shadow-md shadow-brand-green/25 hover:brightness-105"
|
| 97 |
+
: "bg-neutral-100 text-neutral-300 cursor-not-allowed"
|
| 98 |
+
}`}
|
| 99 |
+
aria-label="Send"
|
| 100 |
+
>
|
| 101 |
+
<SendHorizontal className="h-4 w-4" />
|
| 102 |
+
</button>
|
| 103 |
+
)}
|
| 104 |
+
</div>
|
| 105 |
+
</div>
|
| 106 |
+
);
|
| 107 |
+
}
|
src/app/components/chat/ChatLayout.tsx
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ReactNode } from "react";
|
| 2 |
+
|
| 3 |
+
interface ChatLayoutProps {
|
| 4 |
+
sidebar: ReactNode;
|
| 5 |
+
children: ReactNode;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export default function ChatLayout({ sidebar, children }: ChatLayoutProps) {
|
| 9 |
+
return (
|
| 10 |
+
<div className="flex h-screen bg-neutral-50 overflow-hidden">
|
| 11 |
+
{sidebar}
|
| 12 |
+
<div className="flex-1 flex flex-col min-w-0 relative">
|
| 13 |
+
{children}
|
| 14 |
+
</div>
|
| 15 |
+
</div>
|
| 16 |
+
);
|
| 17 |
+
}
|
src/app/components/chat/ChatWindow.tsx
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useRef } from "react";
|
| 2 |
+
import { motion } from "motion/react";
|
| 3 |
+
import { Bot } from "lucide-react";
|
| 4 |
+
import type { Message } from "./types";
|
| 5 |
+
import MessageBubble from "./MessageBubble";
|
| 6 |
+
import maintivalogo from "@/assets/maintiva-logo.jpg";
|
| 7 |
+
|
| 8 |
+
const WELCOME_FEATURES = [
|
| 9 |
+
{ emoji: "📊", title: "Data Analysis", desc: "Get actionable insights and root cause" },
|
| 10 |
+
{ emoji: "🧠", title: "Knowledge Based", desc: "Handbook, SOP, Documents, or Databases" },
|
| 11 |
+
{ emoji: "🎤", title: "Voice Mode", desc: "Ask questions using your voice" },
|
| 12 |
+
{ emoji: "💬", title: "Natural language", desc: "Natural interaction" },
|
| 13 |
+
];
|
| 14 |
+
|
| 15 |
+
interface ChatWindowProps {
|
| 16 |
+
messages: Message[];
|
| 17 |
+
isLoading: boolean;
|
| 18 |
+
streamingMsgId: string | null;
|
| 19 |
+
userName?: string;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
export default function ChatWindow({ messages, isLoading, streamingMsgId, userName }: ChatWindowProps) {
|
| 23 |
+
const firstName = userName?.split(" ")[0] ?? "";
|
| 24 |
+
const bottomRef = useRef<HTMLDivElement>(null);
|
| 25 |
+
|
| 26 |
+
useEffect(() => {
|
| 27 |
+
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
| 28 |
+
}, [messages, isLoading]);
|
| 29 |
+
|
| 30 |
+
if (messages.length === 0 && !isLoading) {
|
| 31 |
+
return (
|
| 32 |
+
<div className="flex-1 overflow-y-auto flex flex-col items-center justify-center gap-6 px-4 sm:px-6 xl:px-8 pb-4">
|
| 33 |
+
<motion.div
|
| 34 |
+
className="flex flex-col items-center gap-4 xl:gap-6 w-full max-w-sm sm:max-w-md xl:max-w-2xl"
|
| 35 |
+
initial={{ opacity: 0, scale: 0.9 }}
|
| 36 |
+
animate={{ opacity: 1, scale: 1 }}
|
| 37 |
+
transition={{ duration: 0.5, ease: "easeOut" }}
|
| 38 |
+
>
|
| 39 |
+
<div className="w-20 h-20 xl:w-28 xl:h-28 rounded-3xl bg-gradient-to-br from-brand-green-light to-brand-green shadow-xl shadow-brand-green/25 flex items-center justify-center">
|
| 40 |
+
<Bot className="h-10 w-10 xl:h-14 xl:w-14 text-white" />
|
| 41 |
+
</div>
|
| 42 |
+
|
| 43 |
+
<div className="text-center">
|
| 44 |
+
<h2 className="text-2xl xl:text-4xl font-bold text-neutral-900">
|
| 45 |
+
{firstName ? `Hi, ${firstName}! 👋` : "Hello! 👋"}
|
| 46 |
+
</h2>
|
| 47 |
+
<p className="mt-2 text-neutral-500 text-sm xl:text-base leading-relaxed">
|
| 48 |
+
Welcome to{" "}
|
| 49 |
+
<span className="inline-flex items-center gap-1 font-bold text-neutral-800 align-middle">
|
| 50 |
+
<img src={maintivalogo} alt="Maintiva" className="h-4 w-4 xl:h-5 xl:w-5 rounded-sm object-contain" />
|
| 51 |
+
Maintiva Agent
|
| 52 |
+
</span>{" "}
|
| 53 |
+
Turn your data into fast, intelligent decisions. Instantly uncover root causes, spot hidden patterns, and resolve issues before they escalate.
|
| 54 |
+
</p>
|
| 55 |
+
</div>
|
| 56 |
+
|
| 57 |
+
<div className="grid grid-cols-2 xl:grid-cols-4 gap-3 xl:gap-4 w-full mt-2">
|
| 58 |
+
{WELCOME_FEATURES.map((f) => (
|
| 59 |
+
<div
|
| 60 |
+
key={f.title}
|
| 61 |
+
className="flex flex-col gap-1 xl:gap-2 p-3 xl:p-4 rounded-xl bg-white border border-neutral-100 shadow-sm text-left"
|
| 62 |
+
>
|
| 63 |
+
<span className="text-xl xl:text-3xl">{f.emoji}</span>
|
| 64 |
+
<span className="text-sm xl:text-base font-semibold text-neutral-800">{f.title}</span>
|
| 65 |
+
<span className="text-xs xl:text-sm text-neutral-400">{f.desc}</span>
|
| 66 |
+
</div>
|
| 67 |
+
))}
|
| 68 |
+
</div>
|
| 69 |
+
</motion.div>
|
| 70 |
+
</div>
|
| 71 |
+
);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
return (
|
| 75 |
+
<div className="flex-1 overflow-y-auto py-4 space-y-1">
|
| 76 |
+
{/* Constrain message width on large/seminar screens */}
|
| 77 |
+
<div className="w-full max-w-4xl xl:max-w-5xl mx-auto">
|
| 78 |
+
{messages.map((message) => (
|
| 79 |
+
<MessageBubble
|
| 80 |
+
key={message.id}
|
| 81 |
+
message={message}
|
| 82 |
+
isStreamingPlaceholder={streamingMsgId === message.id}
|
| 83 |
+
/>
|
| 84 |
+
))}
|
| 85 |
+
<div ref={bottomRef} />
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
);
|
| 89 |
+
}
|
src/app/components/chat/FeedbackWidget.tsx
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useRef } from "react";
|
| 2 |
+
import { ThumbsUp, ThumbsDown, Copy, Check, Loader2, Volume2, VolumeX } from "lucide-react";
|
| 3 |
+
import { replayAudio } from "../../../audio/AudioPlayer";
|
| 4 |
+
import { textToSpeech } from "../../../services/voiceApi";
|
| 5 |
+
|
| 6 |
+
interface FeedbackWidgetProps {
|
| 7 |
+
messageId: string;
|
| 8 |
+
content: string;
|
| 9 |
+
audioText?: string;
|
| 10 |
+
audioChunks?: ArrayBuffer[];
|
| 11 |
+
audioSampleRate?: number;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export default function FeedbackWidget({
|
| 15 |
+
content,
|
| 16 |
+
audioText,
|
| 17 |
+
audioChunks,
|
| 18 |
+
audioSampleRate,
|
| 19 |
+
}: FeedbackWidgetProps) {
|
| 20 |
+
const [thumbsUp, setThumbsUp] = useState(false);
|
| 21 |
+
const [thumbsDown, setThumbsDown] = useState(false);
|
| 22 |
+
const [copyState, setCopyState] = useState<"idle" | "loading" | "success">("idle");
|
| 23 |
+
const [speakerState, setSpeakerState] = useState<"idle" | "loading" | "playing">("idle");
|
| 24 |
+
const cancelPlayRef = useRef<(() => void) | null>(null);
|
| 25 |
+
|
| 26 |
+
const handleThumbsUp = () => {
|
| 27 |
+
setThumbsUp((v) => !v);
|
| 28 |
+
if (thumbsDown) setThumbsDown(false);
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
const handleThumbsDown = () => {
|
| 32 |
+
setThumbsDown((v) => !v);
|
| 33 |
+
if (thumbsUp) setThumbsUp(false);
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
const handleCopy = async () => {
|
| 37 |
+
if (copyState !== "idle") return;
|
| 38 |
+
setCopyState("loading");
|
| 39 |
+
try {
|
| 40 |
+
await navigator.clipboard.writeText(content);
|
| 41 |
+
setCopyState("success");
|
| 42 |
+
setTimeout(() => setCopyState("idle"), 2000);
|
| 43 |
+
} catch {
|
| 44 |
+
setCopyState("idle");
|
| 45 |
+
}
|
| 46 |
+
};
|
| 47 |
+
|
| 48 |
+
const handleSpeaker = async () => {
|
| 49 |
+
if (speakerState === "playing") {
|
| 50 |
+
cancelPlayRef.current?.();
|
| 51 |
+
cancelPlayRef.current = null;
|
| 52 |
+
setSpeakerState("idle");
|
| 53 |
+
return;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
if (speakerState === "loading") return;
|
| 57 |
+
|
| 58 |
+
if (audioChunks && audioChunks.length > 0) {
|
| 59 |
+
// Voice mode: replay pre-stored audio immediately
|
| 60 |
+
setSpeakerState("playing");
|
| 61 |
+
const cancel = replayAudio(audioChunks, audioSampleRate ?? 24000);
|
| 62 |
+
cancelPlayRef.current = cancel;
|
| 63 |
+
// Calculate approx duration to reset state
|
| 64 |
+
const totalSamples = audioChunks.reduce((acc, c) => acc + c.byteLength / 2, 0);
|
| 65 |
+
const duration = (totalSamples / (audioSampleRate ?? 24000)) * 1000;
|
| 66 |
+
setTimeout(() => {
|
| 67 |
+
cancelPlayRef.current = null;
|
| 68 |
+
setSpeakerState("idle");
|
| 69 |
+
}, duration + 200);
|
| 70 |
+
} else {
|
| 71 |
+
// Text mode: request TTS now
|
| 72 |
+
setSpeakerState("loading");
|
| 73 |
+
try {
|
| 74 |
+
const { pcm, sampleRate } = await textToSpeech(audioText || content);
|
| 75 |
+
setSpeakerState("playing");
|
| 76 |
+
const cancel = replayAudio([pcm], sampleRate);
|
| 77 |
+
cancelPlayRef.current = cancel;
|
| 78 |
+
const duration = (pcm.byteLength / 2 / sampleRate) * 1000;
|
| 79 |
+
setTimeout(() => {
|
| 80 |
+
cancelPlayRef.current = null;
|
| 81 |
+
setSpeakerState("idle");
|
| 82 |
+
}, duration + 200);
|
| 83 |
+
} catch {
|
| 84 |
+
setSpeakerState("idle");
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
return (
|
| 90 |
+
<div className="flex items-center gap-1 ml-10 mt-1 px-4">
|
| 91 |
+
<button
|
| 92 |
+
onClick={handleThumbsUp}
|
| 93 |
+
className={`p-1 rounded transition-colors ${
|
| 94 |
+
thumbsUp ? "text-brand-green" : "text-neutral-300 hover:text-neutral-500"
|
| 95 |
+
}`}
|
| 96 |
+
aria-label="Thumbs up"
|
| 97 |
+
>
|
| 98 |
+
<ThumbsUp className="h-3.5 w-3.5" />
|
| 99 |
+
</button>
|
| 100 |
+
|
| 101 |
+
<button
|
| 102 |
+
onClick={handleThumbsDown}
|
| 103 |
+
className={`p-1 rounded transition-colors ${
|
| 104 |
+
thumbsDown ? "text-red-400" : "text-neutral-300 hover:text-neutral-500"
|
| 105 |
+
}`}
|
| 106 |
+
aria-label="Thumbs down"
|
| 107 |
+
>
|
| 108 |
+
<ThumbsDown className="h-3.5 w-3.5" />
|
| 109 |
+
</button>
|
| 110 |
+
|
| 111 |
+
<button
|
| 112 |
+
onClick={handleCopy}
|
| 113 |
+
className={`p-1 rounded transition-colors ${
|
| 114 |
+
copyState === "success"
|
| 115 |
+
? "text-brand-green"
|
| 116 |
+
: "text-neutral-300 hover:text-neutral-500"
|
| 117 |
+
}`}
|
| 118 |
+
aria-label="Copy message"
|
| 119 |
+
>
|
| 120 |
+
{copyState === "loading" ? (
|
| 121 |
+
<Loader2 className="h-3.5 w-3.5 animate-spin text-neutral-400" />
|
| 122 |
+
) : copyState === "success" ? (
|
| 123 |
+
<Check className="h-3.5 w-3.5" />
|
| 124 |
+
) : (
|
| 125 |
+
<Copy className="h-3.5 w-3.5" />
|
| 126 |
+
)}
|
| 127 |
+
</button>
|
| 128 |
+
|
| 129 |
+
<button
|
| 130 |
+
onClick={handleSpeaker}
|
| 131 |
+
className={`p-1 rounded transition-colors ${
|
| 132 |
+
speakerState === "playing"
|
| 133 |
+
? "text-brand-green"
|
| 134 |
+
: "text-neutral-300 hover:text-neutral-500"
|
| 135 |
+
}`}
|
| 136 |
+
aria-label="Play audio"
|
| 137 |
+
>
|
| 138 |
+
{speakerState === "loading" ? (
|
| 139 |
+
<Loader2 className="h-3.5 w-3.5 animate-spin text-neutral-400" />
|
| 140 |
+
) : speakerState === "playing" ? (
|
| 141 |
+
<VolumeX className="h-3.5 w-3.5" />
|
| 142 |
+
) : (
|
| 143 |
+
<Volume2 className="h-3.5 w-3.5" />
|
| 144 |
+
)}
|
| 145 |
+
</button>
|
| 146 |
+
</div>
|
| 147 |
+
);
|
| 148 |
+
}
|
src/app/components/chat/MessageBubble.tsx
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect } from "react";
|
| 2 |
+
import { motion } from "motion/react";
|
| 3 |
+
import { Bot } from "lucide-react";
|
| 4 |
+
import type { Message } from "./types";
|
| 5 |
+
import TypingIndicator from "./TypingIndicator";
|
| 6 |
+
import FeedbackWidget from "./FeedbackWidget";
|
| 7 |
+
import MarkdownRenderer from "./renderers/MarkdownRenderer";
|
| 8 |
+
|
| 9 |
+
interface MessageBubbleProps {
|
| 10 |
+
message: Message;
|
| 11 |
+
isStreamingPlaceholder?: boolean;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export default function MessageBubble({ message, isStreamingPlaceholder }: MessageBubbleProps) {
|
| 15 |
+
useEffect(() => {
|
| 16 |
+
if (message.role === "assistant") {
|
| 17 |
+
// console.log(
|
| 18 |
+
// `[MessageBubble] id=...${message.id.slice(-6)} isStreamingPlaceholder=${isStreamingPlaceholder} contentLen=${message.content.length}`
|
| 19 |
+
// );
|
| 20 |
+
}
|
| 21 |
+
}, [isStreamingPlaceholder, message.id, message.role, message.content.length]);
|
| 22 |
+
|
| 23 |
+
if (message.role === "user") {
|
| 24 |
+
return (
|
| 25 |
+
<motion.div
|
| 26 |
+
className="flex justify-end px-3 sm:px-4 py-1"
|
| 27 |
+
initial={{ opacity: 0, y: 12 }}
|
| 28 |
+
animate={{ opacity: 1, y: 0 }}
|
| 29 |
+
transition={{ duration: 0.3, ease: "easeOut" }}
|
| 30 |
+
>
|
| 31 |
+
<div className="max-w-[88%] sm:max-w-[75%] xl:max-w-[65%] px-4 py-3 rounded-2xl rounded-br-sm bg-gradient-to-br from-brand-green-light to-brand-green text-white text-sm xl:text-base leading-relaxed shadow-md shadow-brand-green/20">
|
| 32 |
+
{message.content}
|
| 33 |
+
</div>
|
| 34 |
+
</motion.div>
|
| 35 |
+
);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// Show TypingIndicator when the assistant placeholder has no content yet
|
| 39 |
+
if (isStreamingPlaceholder && message.content === "") {
|
| 40 |
+
return <TypingIndicator />;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
return (
|
| 44 |
+
<motion.div
|
| 45 |
+
initial={{ opacity: 0, y: 12 }}
|
| 46 |
+
animate={{ opacity: 1, y: 0 }}
|
| 47 |
+
transition={{ duration: 0.3, ease: "easeOut" }}
|
| 48 |
+
>
|
| 49 |
+
<div className="flex gap-3 px-3 sm:px-4 py-2">
|
| 50 |
+
<div className="w-7 h-7 xl:w-8 xl:h-8 rounded-full bg-gradient-to-br from-brand-green-light to-brand-green flex-shrink-0 flex items-center justify-center shadow-sm">
|
| 51 |
+
<Bot className="h-4 w-4 text-white" />
|
| 52 |
+
</div>
|
| 53 |
+
|
| 54 |
+
<div className="max-w-[92%] sm:max-w-[88%] xl:max-w-[80%] bg-white border border-neutral-100 rounded-2xl rounded-tl-sm shadow-sm px-4 xl:px-5 py-3 xl:py-4">
|
| 55 |
+
<MarkdownRenderer
|
| 56 |
+
key={isStreamingPlaceholder ? "streaming" : "final"}
|
| 57 |
+
content={message.content}
|
| 58 |
+
skipPreprocess={isStreamingPlaceholder}
|
| 59 |
+
/>
|
| 60 |
+
|
| 61 |
+
{message.sources && message.sources.length > 0 && (
|
| 62 |
+
<div className="mt-3 pt-2 border-t border-neutral-100">
|
| 63 |
+
<p className="text-[10px] text-neutral-400 mb-1.5">Sources:</p>
|
| 64 |
+
<div className="flex flex-wrap gap-1">
|
| 65 |
+
{message.sources.map((src, i) => (
|
| 66 |
+
<span
|
| 67 |
+
key={i}
|
| 68 |
+
className="text-[10px] bg-neutral-100 text-neutral-600 px-2 py-0.5 rounded-full border border-neutral-200"
|
| 69 |
+
title={src.page_label ? `Page ${src.page_label}` : undefined}
|
| 70 |
+
>
|
| 71 |
+
📄 {src.filename}
|
| 72 |
+
{src.page_label ? ` p.${src.page_label}` : ""}
|
| 73 |
+
</span>
|
| 74 |
+
))}
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
+
)}
|
| 78 |
+
</div>
|
| 79 |
+
</div>
|
| 80 |
+
|
| 81 |
+
{!isStreamingPlaceholder && (
|
| 82 |
+
<FeedbackWidget
|
| 83 |
+
messageId={message.id}
|
| 84 |
+
content={message.content}
|
| 85 |
+
audioText={message.audioText}
|
| 86 |
+
audioChunks={message.audioChunks}
|
| 87 |
+
audioSampleRate={message.audioSampleRate}
|
| 88 |
+
/>
|
| 89 |
+
)}
|
| 90 |
+
</motion.div>
|
| 91 |
+
);
|
| 92 |
+
}
|
src/app/components/chat/Sidebar.tsx
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from "react";
|
| 2 |
+
import { motion, AnimatePresence } from "motion/react";
|
| 3 |
+
import {
|
| 4 |
+
Plus,
|
| 5 |
+
MessageSquare,
|
| 6 |
+
Trash2,
|
| 7 |
+
ChevronLeft,
|
| 8 |
+
ChevronRight,
|
| 9 |
+
Settings,
|
| 10 |
+
LogOut,
|
| 11 |
+
} from "lucide-react";
|
| 12 |
+
import maintivalogoUrl from "../../../assets/maintiva-logo.jpg";
|
| 13 |
+
import type { ChatSession, StoredUser } from "./types";
|
| 14 |
+
|
| 15 |
+
interface SidebarProps {
|
| 16 |
+
sessions: ChatSession[];
|
| 17 |
+
activeSessionId: string | null;
|
| 18 |
+
isLoadingSessions: boolean;
|
| 19 |
+
user: StoredUser | null;
|
| 20 |
+
onNewChat: () => void;
|
| 21 |
+
onSelectSession: (id: string) => void;
|
| 22 |
+
onDeleteSession: (id: string) => void;
|
| 23 |
+
onLogout: () => void;
|
| 24 |
+
mobileOpen: boolean;
|
| 25 |
+
onMobileClose: () => void;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
function formatTime(dateStr: string | null | undefined): string {
|
| 29 |
+
if (!dateStr) return "";
|
| 30 |
+
const d = new Date(dateStr);
|
| 31 |
+
const now = new Date();
|
| 32 |
+
const diffMs = now.getTime() - d.getTime();
|
| 33 |
+
const diffDays = Math.floor(diffMs / 86400000);
|
| 34 |
+
if (diffDays === 0) {
|
| 35 |
+
return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
| 36 |
+
}
|
| 37 |
+
if (diffDays === 1) return "Yesterday";
|
| 38 |
+
if (diffDays < 7) return `${diffDays}d ago`;
|
| 39 |
+
return d.toLocaleDateString([], { month: "short", day: "numeric" });
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
export default function Sidebar({
|
| 43 |
+
sessions,
|
| 44 |
+
activeSessionId,
|
| 45 |
+
isLoadingSessions,
|
| 46 |
+
user,
|
| 47 |
+
onNewChat,
|
| 48 |
+
onSelectSession,
|
| 49 |
+
onDeleteSession,
|
| 50 |
+
onLogout,
|
| 51 |
+
mobileOpen,
|
| 52 |
+
onMobileClose,
|
| 53 |
+
}: SidebarProps) {
|
| 54 |
+
const [collapsed, setCollapsed] = useState(false);
|
| 55 |
+
const [hoveredSession, setHoveredSession] = useState<string | null>(null);
|
| 56 |
+
|
| 57 |
+
return (
|
| 58 |
+
<>
|
| 59 |
+
{/* Mobile backdrop */}
|
| 60 |
+
<AnimatePresence>
|
| 61 |
+
{mobileOpen && (
|
| 62 |
+
<motion.div
|
| 63 |
+
className="fixed inset-0 bg-black/40 z-40 md:hidden"
|
| 64 |
+
initial={{ opacity: 0 }}
|
| 65 |
+
animate={{ opacity: 1 }}
|
| 66 |
+
exit={{ opacity: 0 }}
|
| 67 |
+
transition={{ duration: 0.2 }}
|
| 68 |
+
onClick={onMobileClose}
|
| 69 |
+
/>
|
| 70 |
+
)}
|
| 71 |
+
</AnimatePresence>
|
| 72 |
+
|
| 73 |
+
<motion.div
|
| 74 |
+
className={[
|
| 75 |
+
"flex flex-col h-full flex-shrink-0 bg-white border-r border-neutral-100 overflow-hidden",
|
| 76 |
+
// Mobile: fixed overlay drawer; desktop: in-flow
|
| 77 |
+
"fixed inset-y-0 left-0 z-50 md:relative md:inset-auto md:z-auto",
|
| 78 |
+
// Mobile slide in/out; always visible on desktop
|
| 79 |
+
mobileOpen ? "translate-x-0" : "-translate-x-full",
|
| 80 |
+
"md:translate-x-0",
|
| 81 |
+
// CSS transition for mobile only; framer-motion handles desktop width
|
| 82 |
+
"transition-transform duration-300 ease-in-out md:transition-none",
|
| 83 |
+
// Never overflow phone screen
|
| 84 |
+
"max-w-[85vw] md:max-w-none",
|
| 85 |
+
].join(" ")}
|
| 86 |
+
animate={{ width: collapsed ? 72 : 280 }}
|
| 87 |
+
transition={{ duration: 0.3, ease: "easeInOut" }}
|
| 88 |
+
>
|
| 89 |
+
{/* Collapse toggle — desktop only */}
|
| 90 |
+
<button
|
| 91 |
+
onClick={() => setCollapsed((v) => !v)}
|
| 92 |
+
className="hidden md:flex absolute -right-3 top-1/2 -translate-y-1/2 z-20 w-6 h-6 rounded-full bg-white border border-neutral-200 shadow-sm text-neutral-500 hover:text-brand-green hover:border-brand-green transition-all items-center justify-center"
|
| 93 |
+
aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
|
| 94 |
+
>
|
| 95 |
+
{collapsed ? (
|
| 96 |
+
<ChevronRight className="h-3.5 w-3.5" />
|
| 97 |
+
) : (
|
| 98 |
+
<ChevronLeft className="h-3.5 w-3.5" />
|
| 99 |
+
)}
|
| 100 |
+
</button>
|
| 101 |
+
|
| 102 |
+
{/* Header */}
|
| 103 |
+
<div className="flex items-center gap-3 px-4 py-4 border-b border-neutral-100">
|
| 104 |
+
<motion.div
|
| 105 |
+
className="w-9 h-9 rounded-xl overflow-hidden flex-shrink-0 shadow-md"
|
| 106 |
+
whileHover={{ scale: 1.05 }}
|
| 107 |
+
>
|
| 108 |
+
<img src={maintivalogoUrl} alt="Maintiva" className="w-full h-full object-cover" />
|
| 109 |
+
</motion.div>
|
| 110 |
+
|
| 111 |
+
<AnimatePresence>
|
| 112 |
+
{!collapsed && (
|
| 113 |
+
<motion.div
|
| 114 |
+
initial={{ opacity: 0, width: 0 }}
|
| 115 |
+
animate={{ opacity: 1, width: "auto" }}
|
| 116 |
+
exit={{ opacity: 0, width: 0 }}
|
| 117 |
+
transition={{ duration: 0.2 }}
|
| 118 |
+
className="overflow-hidden"
|
| 119 |
+
>
|
| 120 |
+
<p className="font-bold text-neutral-900 text-base whitespace-nowrap">Maintiva Agent</p>
|
| 121 |
+
<p className="text-xs text-neutral-400 whitespace-nowrap">AI Data Assistant</p>
|
| 122 |
+
</motion.div>
|
| 123 |
+
)}
|
| 124 |
+
</AnimatePresence>
|
| 125 |
+
</div>
|
| 126 |
+
|
| 127 |
+
{/* New Chat button */}
|
| 128 |
+
<div className="px-3 py-3">
|
| 129 |
+
<button
|
| 130 |
+
onClick={() => { onNewChat(); onMobileClose(); }}
|
| 131 |
+
className={`w-full flex items-center gap-3 rounded-xl px-3 py-2.5 bg-gradient-to-br from-brand-green-light to-brand-green text-white text-sm font-semibold shadow-md shadow-brand-green/20 hover:shadow-lg hover:shadow-brand-green/25 hover:brightness-105 transition-all ${
|
| 132 |
+
collapsed ? "justify-center px-0" : ""
|
| 133 |
+
}`}
|
| 134 |
+
>
|
| 135 |
+
<Plus className="h-4 w-4 flex-shrink-0" />
|
| 136 |
+
<AnimatePresence>
|
| 137 |
+
{!collapsed && (
|
| 138 |
+
<motion.span
|
| 139 |
+
initial={{ opacity: 0, width: 0 }}
|
| 140 |
+
animate={{ opacity: 1, width: "auto" }}
|
| 141 |
+
exit={{ opacity: 0, width: 0 }}
|
| 142 |
+
transition={{ duration: 0.2 }}
|
| 143 |
+
className="overflow-hidden whitespace-nowrap"
|
| 144 |
+
>
|
| 145 |
+
New Chat
|
| 146 |
+
</motion.span>
|
| 147 |
+
)}
|
| 148 |
+
</AnimatePresence>
|
| 149 |
+
</button>
|
| 150 |
+
</div>
|
| 151 |
+
|
| 152 |
+
{/* Sessions */}
|
| 153 |
+
<div className="flex-1 overflow-y-auto">
|
| 154 |
+
{!collapsed && sessions.length > 0 && (
|
| 155 |
+
<p className="text-xs font-semibold text-neutral-400 uppercase tracking-wider px-4 pt-2 pb-1">
|
| 156 |
+
Recent
|
| 157 |
+
</p>
|
| 158 |
+
)}
|
| 159 |
+
|
| 160 |
+
{isLoadingSessions ? (
|
| 161 |
+
<div className="flex justify-center py-4">
|
| 162 |
+
<div className="w-4 h-4 rounded-full border-2 border-brand-green border-t-transparent animate-spin" />
|
| 163 |
+
</div>
|
| 164 |
+
) : (
|
| 165 |
+
<div className="px-2 space-y-0.5">
|
| 166 |
+
{sessions.map((session) => {
|
| 167 |
+
const isActive = session.id === activeSessionId;
|
| 168 |
+
return (
|
| 169 |
+
<div
|
| 170 |
+
key={session.id}
|
| 171 |
+
className={`relative flex items-center gap-3 rounded-xl px-3 py-2.5 cursor-pointer transition-all duration-150 ${
|
| 172 |
+
isActive
|
| 173 |
+
? "bg-brand-green-50 text-brand-green"
|
| 174 |
+
: "text-neutral-700 hover:bg-neutral-50"
|
| 175 |
+
}`}
|
| 176 |
+
onClick={() => { onSelectSession(session.id); onMobileClose(); }}
|
| 177 |
+
onMouseEnter={() => setHoveredSession(session.id)}
|
| 178 |
+
onMouseLeave={() => setHoveredSession(null)}
|
| 179 |
+
>
|
| 180 |
+
<MessageSquare
|
| 181 |
+
className={`h-4 w-4 flex-shrink-0 ${
|
| 182 |
+
isActive ? "text-brand-green" : "text-neutral-400"
|
| 183 |
+
}`}
|
| 184 |
+
/>
|
| 185 |
+
|
| 186 |
+
<AnimatePresence>
|
| 187 |
+
{!collapsed && (
|
| 188 |
+
<motion.div
|
| 189 |
+
initial={{ opacity: 0, width: 0 }}
|
| 190 |
+
animate={{ opacity: 1, width: "auto" }}
|
| 191 |
+
exit={{ opacity: 0, width: 0 }}
|
| 192 |
+
transition={{ duration: 0.2 }}
|
| 193 |
+
className="flex-1 min-w-0 overflow-hidden"
|
| 194 |
+
>
|
| 195 |
+
<p className="text-sm font-medium truncate">{session.title}</p>
|
| 196 |
+
<p className="text-xs text-neutral-400 truncate">
|
| 197 |
+
{formatTime(session.updatedAt ?? session.createdAt)}
|
| 198 |
+
</p>
|
| 199 |
+
</motion.div>
|
| 200 |
+
)}
|
| 201 |
+
</AnimatePresence>
|
| 202 |
+
|
| 203 |
+
{!collapsed && hoveredSession === session.id && (
|
| 204 |
+
<button
|
| 205 |
+
onClick={(e) => {
|
| 206 |
+
e.stopPropagation();
|
| 207 |
+
onDeleteSession(session.id);
|
| 208 |
+
}}
|
| 209 |
+
className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 rounded-lg text-neutral-400 hover:text-red-500 hover:bg-red-50 transition-all"
|
| 210 |
+
aria-label="Delete session"
|
| 211 |
+
>
|
| 212 |
+
<Trash2 className="h-3.5 w-3.5" />
|
| 213 |
+
</button>
|
| 214 |
+
)}
|
| 215 |
+
</div>
|
| 216 |
+
);
|
| 217 |
+
})}
|
| 218 |
+
</div>
|
| 219 |
+
)}
|
| 220 |
+
</div>
|
| 221 |
+
|
| 222 |
+
{/* Footer */}
|
| 223 |
+
<div className="border-t border-neutral-100 p-3">
|
| 224 |
+
<div className="flex items-center gap-3 rounded-xl px-2 py-2">
|
| 225 |
+
<div className="h-7 w-7 rounded-full bg-gradient-to-br from-brand-green-light to-brand-green flex items-center justify-center flex-shrink-0">
|
| 226 |
+
<span className="text-xs font-bold text-white">
|
| 227 |
+
{user?.name?.[0]?.toUpperCase() ?? "U"}
|
| 228 |
+
</span>
|
| 229 |
+
</div>
|
| 230 |
+
|
| 231 |
+
<AnimatePresence>
|
| 232 |
+
{!collapsed && (
|
| 233 |
+
<motion.div
|
| 234 |
+
initial={{ opacity: 0, width: 0 }}
|
| 235 |
+
animate={{ opacity: 1, width: "auto" }}
|
| 236 |
+
exit={{ opacity: 0, width: 0 }}
|
| 237 |
+
transition={{ duration: 0.2 }}
|
| 238 |
+
className="flex-1 min-w-0 overflow-hidden"
|
| 239 |
+
>
|
| 240 |
+
<p className="text-sm font-semibold text-neutral-900 truncate whitespace-nowrap">
|
| 241 |
+
{user?.name ?? "User"}
|
| 242 |
+
</p>
|
| 243 |
+
<p className="text-xs text-neutral-400 truncate whitespace-nowrap">
|
| 244 |
+
{user?.email ?? ""}
|
| 245 |
+
</p>
|
| 246 |
+
</motion.div>
|
| 247 |
+
)}
|
| 248 |
+
</AnimatePresence>
|
| 249 |
+
|
| 250 |
+
{!collapsed && (
|
| 251 |
+
<div className="flex items-center gap-0.5">
|
| 252 |
+
<button
|
| 253 |
+
className="p-1.5 rounded-lg text-neutral-400 hover:text-brand-green hover:bg-brand-green-50 transition-all"
|
| 254 |
+
aria-label="Settings"
|
| 255 |
+
>
|
| 256 |
+
<Settings className="h-4 w-4" />
|
| 257 |
+
</button>
|
| 258 |
+
<button
|
| 259 |
+
onClick={onLogout}
|
| 260 |
+
className="p-1.5 rounded-lg text-neutral-400 hover:text-red-500 hover:bg-red-50 transition-all"
|
| 261 |
+
aria-label="Logout"
|
| 262 |
+
>
|
| 263 |
+
<LogOut className="h-4 w-4" />
|
| 264 |
+
</button>
|
| 265 |
+
</div>
|
| 266 |
+
)}
|
| 267 |
+
|
| 268 |
+
{collapsed && (
|
| 269 |
+
<button
|
| 270 |
+
onClick={onLogout}
|
| 271 |
+
className="p-1.5 rounded-lg text-neutral-400 hover:text-red-500 hover:bg-red-50 transition-all"
|
| 272 |
+
aria-label="Logout"
|
| 273 |
+
>
|
| 274 |
+
<LogOut className="h-4 w-4" />
|
| 275 |
+
</button>
|
| 276 |
+
)}
|
| 277 |
+
</div>
|
| 278 |
+
</div>
|
| 279 |
+
</motion.div>
|
| 280 |
+
</>
|
| 281 |
+
);
|
| 282 |
+
}
|
src/app/components/chat/TypingIndicator.tsx
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect } from "react";
|
| 2 |
+
import { motion } from "motion/react";
|
| 3 |
+
import { Bot } from "lucide-react";
|
| 4 |
+
|
| 5 |
+
function useLoadingMessages() {
|
| 6 |
+
const [messages, setMessages] = useState<string[]>([]);
|
| 7 |
+
|
| 8 |
+
useEffect(() => {
|
| 9 |
+
fetch("/loading-messages.yaml")
|
| 10 |
+
.then((r) => r.text())
|
| 11 |
+
.then((text) => {
|
| 12 |
+
const parsed = text
|
| 13 |
+
.split("\n")
|
| 14 |
+
.filter((l) => l.trimStart().startsWith("- "))
|
| 15 |
+
.map((l) => l.replace(/^\s*- /, "").trim())
|
| 16 |
+
.filter(Boolean);
|
| 17 |
+
if (parsed.length > 0) setMessages(parsed);
|
| 18 |
+
})
|
| 19 |
+
.catch(() => {});
|
| 20 |
+
}, []);
|
| 21 |
+
|
| 22 |
+
return messages;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export default function TypingIndicator() {
|
| 26 |
+
const loadingMessages = useLoadingMessages();
|
| 27 |
+
const [phraseIndex, setPhraseIndex] = useState(0);
|
| 28 |
+
|
| 29 |
+
useEffect(() => {
|
| 30 |
+
if (loadingMessages.length === 0) return;
|
| 31 |
+
setPhraseIndex(Math.floor(Math.random() * loadingMessages.length));
|
| 32 |
+
const id = setInterval(() => {
|
| 33 |
+
setPhraseIndex((prev) => {
|
| 34 |
+
let next: number;
|
| 35 |
+
do {
|
| 36 |
+
next = Math.floor(Math.random() * loadingMessages.length);
|
| 37 |
+
} while (loadingMessages.length > 1 && next === prev);
|
| 38 |
+
return next;
|
| 39 |
+
});
|
| 40 |
+
}, 300);
|
| 41 |
+
return () => clearInterval(id);
|
| 42 |
+
}, [loadingMessages]);
|
| 43 |
+
|
| 44 |
+
return (
|
| 45 |
+
<motion.div
|
| 46 |
+
className="flex gap-3 px-4 py-2"
|
| 47 |
+
initial={{ opacity: 0, y: 8 }}
|
| 48 |
+
animate={{ opacity: 1, y: 0 }}
|
| 49 |
+
transition={{ duration: 0.25, ease: "easeOut" }}
|
| 50 |
+
>
|
| 51 |
+
<div className="w-7 h-7 rounded-full bg-gradient-to-br from-brand-green-light to-brand-green flex-shrink-0 flex items-center justify-center shadow-sm">
|
| 52 |
+
<Bot className="h-4 w-4 text-white" />
|
| 53 |
+
</div>
|
| 54 |
+
|
| 55 |
+
<div className="bg-white border border-neutral-100 rounded-2xl rounded-tl-sm shadow-sm px-4 py-3 min-w-[6rem]">
|
| 56 |
+
{loadingMessages.length > 0 ? (
|
| 57 |
+
<span className="text-sm text-neutral-400 font-medium">
|
| 58 |
+
{loadingMessages[phraseIndex]}…
|
| 59 |
+
</span>
|
| 60 |
+
) : (
|
| 61 |
+
<div className="flex items-center gap-1.5 animate-pulse">
|
| 62 |
+
<span className="w-2 h-2 rounded-full bg-neutral-300" />
|
| 63 |
+
<span className="w-2 h-2 rounded-full bg-neutral-300" style={{ animationDelay: "150ms" }} />
|
| 64 |
+
<span className="w-2 h-2 rounded-full bg-neutral-300" style={{ animationDelay: "300ms" }} />
|
| 65 |
+
</div>
|
| 66 |
+
)}
|
| 67 |
+
</div>
|
| 68 |
+
</motion.div>
|
| 69 |
+
);
|
| 70 |
+
}
|
src/app/components/chat/VoiceMicButton.tsx
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { motion } from "motion/react";
|
| 2 |
+
import { Loader2, Mic, MicOff, Volume2 } from "lucide-react";
|
| 3 |
+
import type { VoiceState } from "../../../hooks/useVoiceSession";
|
| 4 |
+
|
| 5 |
+
interface VoiceMicButtonProps {
|
| 6 |
+
voiceState: VoiceState;
|
| 7 |
+
onToggle: () => void;
|
| 8 |
+
disabled?: boolean;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export default function VoiceMicButton({ voiceState, onToggle, disabled }: VoiceMicButtonProps) {
|
| 12 |
+
const isDisabled = disabled ?? false;
|
| 13 |
+
|
| 14 |
+
const stateConfig: Record<
|
| 15 |
+
VoiceState,
|
| 16 |
+
{ icon: React.ReactNode; className: string; title: string; pulse: boolean; scalePulse: boolean }
|
| 17 |
+
> = {
|
| 18 |
+
IDLE: {
|
| 19 |
+
icon: <Mic className="h-4 w-4" />,
|
| 20 |
+
className: "bg-neutral-100 text-neutral-400 hover:bg-brand-green/10 hover:text-brand-green",
|
| 21 |
+
title: "Start voice session",
|
| 22 |
+
pulse: false,
|
| 23 |
+
scalePulse: false,
|
| 24 |
+
},
|
| 25 |
+
CONNECTING: {
|
| 26 |
+
icon: <Loader2 className="h-4 w-4 animate-spin" />,
|
| 27 |
+
className: "bg-brand-green/10 text-brand-green/60",
|
| 28 |
+
title: "Connecting...",
|
| 29 |
+
pulse: false,
|
| 30 |
+
scalePulse: false,
|
| 31 |
+
},
|
| 32 |
+
LISTENING: {
|
| 33 |
+
icon: <Mic className="h-4 w-4" />,
|
| 34 |
+
className: "bg-brand-green text-white shadow-md shadow-brand-green/25",
|
| 35 |
+
title: "Listening — click to stop",
|
| 36 |
+
pulse: true,
|
| 37 |
+
scalePulse: false,
|
| 38 |
+
},
|
| 39 |
+
PROCESSING: {
|
| 40 |
+
icon: <Loader2 className="h-4 w-4 animate-spin" />,
|
| 41 |
+
className: "bg-brand-amber text-white",
|
| 42 |
+
title: "Processing...",
|
| 43 |
+
pulse: false,
|
| 44 |
+
scalePulse: false,
|
| 45 |
+
},
|
| 46 |
+
SPEAKING: {
|
| 47 |
+
icon: <Volume2 className="h-4 w-4" />,
|
| 48 |
+
className: "bg-brand-cyan text-white",
|
| 49 |
+
title: "Agent is speaking — click to stop",
|
| 50 |
+
pulse: false,
|
| 51 |
+
scalePulse: true,
|
| 52 |
+
},
|
| 53 |
+
ERROR: {
|
| 54 |
+
icon: <MicOff className="h-4 w-4" />,
|
| 55 |
+
className: "bg-red-100 text-red-400 hover:bg-red-200",
|
| 56 |
+
title: "Connection failed — click to retry",
|
| 57 |
+
pulse: false,
|
| 58 |
+
scalePulse: false,
|
| 59 |
+
},
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
const cfg = stateConfig[voiceState];
|
| 63 |
+
|
| 64 |
+
return (
|
| 65 |
+
<div className="relative flex-shrink-0">
|
| 66 |
+
{cfg.pulse && (
|
| 67 |
+
<motion.span
|
| 68 |
+
className="absolute inset-0 rounded-xl bg-brand-green/30"
|
| 69 |
+
animate={{ scale: [1, 1.6], opacity: [0.6, 0] }}
|
| 70 |
+
transition={{ duration: 1.2, repeat: Infinity, ease: "easeOut" }}
|
| 71 |
+
/>
|
| 72 |
+
)}
|
| 73 |
+
<motion.button
|
| 74 |
+
onClick={onToggle}
|
| 75 |
+
disabled={isDisabled}
|
| 76 |
+
title={cfg.title}
|
| 77 |
+
animate={cfg.scalePulse ? { scale: [1, 1.05, 1] } : { scale: 1 }}
|
| 78 |
+
transition={cfg.scalePulse ? { duration: 1.4, repeat: Infinity, ease: "easeInOut" } : {}}
|
| 79 |
+
className={`relative w-9 h-9 rounded-xl flex items-center justify-center transition-all duration-200 ${cfg.className} ${isDisabled ? "pointer-events-none" : ""}`}
|
| 80 |
+
aria-label={cfg.title}
|
| 81 |
+
>
|
| 82 |
+
{cfg.icon}
|
| 83 |
+
</motion.button>
|
| 84 |
+
</div>
|
| 85 |
+
);
|
| 86 |
+
}
|
src/app/components/chat/VoiceStatusBar.tsx
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { X } from "lucide-react";
|
| 2 |
+
import { motion } from "motion/react";
|
| 3 |
+
import type { VoiceState } from "../../../hooks/useVoiceSession";
|
| 4 |
+
|
| 5 |
+
interface VoiceStatusBarProps {
|
| 6 |
+
voiceState: VoiceState;
|
| 7 |
+
onStop: () => void;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const STATE_LABELS: Record<VoiceState, string> = {
|
| 11 |
+
IDLE: "",
|
| 12 |
+
CONNECTING: "Connecting...",
|
| 13 |
+
LISTENING: "Listening...",
|
| 14 |
+
PROCESSING: "Processing...",
|
| 15 |
+
SPEAKING: "Agent is speaking",
|
| 16 |
+
ERROR: "Connection error",
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
const STATE_COLORS: Record<VoiceState, string> = {
|
| 20 |
+
IDLE: "",
|
| 21 |
+
CONNECTING: "bg-brand-green/10 text-brand-green border-brand-green/20",
|
| 22 |
+
LISTENING: "bg-brand-green/10 text-brand-green border-brand-green/20",
|
| 23 |
+
PROCESSING: "bg-brand-amber/10 text-brand-amber border-brand-amber/20",
|
| 24 |
+
SPEAKING: "bg-brand-cyan/10 text-brand-cyan border-brand-cyan/20",
|
| 25 |
+
ERROR: "bg-red-50 text-red-500 border-red-200",
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
export default function VoiceStatusBar({ voiceState, onStop }: VoiceStatusBarProps) {
|
| 29 |
+
return (
|
| 30 |
+
<motion.div
|
| 31 |
+
initial={{ opacity: 0, y: 6 }}
|
| 32 |
+
animate={{ opacity: 1, y: 0 }}
|
| 33 |
+
exit={{ opacity: 0, y: 6 }}
|
| 34 |
+
transition={{ duration: 0.2 }}
|
| 35 |
+
className={`flex items-center justify-between gap-2 px-3 py-1.5 rounded-xl border text-xs font-medium ${STATE_COLORS[voiceState]}`}
|
| 36 |
+
>
|
| 37 |
+
<div className="flex items-center gap-2">
|
| 38 |
+
<span className="relative flex h-2 w-2">
|
| 39 |
+
<span
|
| 40 |
+
className={`animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 ${
|
| 41 |
+
voiceState === "LISTENING"
|
| 42 |
+
? "bg-brand-green"
|
| 43 |
+
: voiceState === "SPEAKING"
|
| 44 |
+
? "bg-brand-cyan"
|
| 45 |
+
: voiceState === "PROCESSING"
|
| 46 |
+
? "bg-brand-amber"
|
| 47 |
+
: "bg-neutral-400"
|
| 48 |
+
}`}
|
| 49 |
+
/>
|
| 50 |
+
<span
|
| 51 |
+
className={`relative inline-flex rounded-full h-2 w-2 ${
|
| 52 |
+
voiceState === "LISTENING"
|
| 53 |
+
? "bg-brand-green"
|
| 54 |
+
: voiceState === "SPEAKING"
|
| 55 |
+
? "bg-brand-cyan"
|
| 56 |
+
: voiceState === "PROCESSING"
|
| 57 |
+
? "bg-brand-amber"
|
| 58 |
+
: "bg-neutral-400"
|
| 59 |
+
}`}
|
| 60 |
+
/>
|
| 61 |
+
</span>
|
| 62 |
+
<span>{STATE_LABELS[voiceState]}</span>
|
| 63 |
+
</div>
|
| 64 |
+
<button
|
| 65 |
+
onClick={onStop}
|
| 66 |
+
className="p-0.5 rounded hover:bg-black/10 transition-colors"
|
| 67 |
+
aria-label="End voice session"
|
| 68 |
+
>
|
| 69 |
+
<X className="h-3.5 w-3.5" />
|
| 70 |
+
</button>
|
| 71 |
+
</motion.div>
|
| 72 |
+
);
|
| 73 |
+
}
|
src/app/components/chat/renderers/MarkdownRenderer.tsx
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ReactMarkdown from "react-markdown";
|
| 2 |
+
import remarkGfm from "remark-gfm";
|
| 3 |
+
import remarkMath from "remark-math";
|
| 4 |
+
import rehypeKatex from "rehype-katex";
|
| 5 |
+
import type { Components } from "react-markdown";
|
| 6 |
+
|
| 7 |
+
function preprocessMarkdown(content: string): string {
|
| 8 |
+
let result = content.replace(/\|\|/g, "|\n|");
|
| 9 |
+
const lines = result.split("\n");
|
| 10 |
+
const processed = lines.map((line) => {
|
| 11 |
+
const tableStart = line.indexOf("|");
|
| 12 |
+
if (tableStart > 0) {
|
| 13 |
+
const tableContent = line.slice(tableStart);
|
| 14 |
+
if ((tableContent.match(/\|/g) ?? []).length >= 2) {
|
| 15 |
+
return line.slice(0, tableStart).trimEnd() + "\n\n" + tableContent;
|
| 16 |
+
}
|
| 17 |
+
}
|
| 18 |
+
return line;
|
| 19 |
+
});
|
| 20 |
+
result = processed.join("\n");
|
| 21 |
+
result = result.replace(/([^|\n])\n(\|)/g, "$1\n\n$2");
|
| 22 |
+
return result;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
const components: Components = {
|
| 26 |
+
p: ({ children }) => (
|
| 27 |
+
<p className="text-sm text-neutral-800 leading-relaxed mb-3 last:mb-0">{children}</p>
|
| 28 |
+
),
|
| 29 |
+
h1: ({ children }) => (
|
| 30 |
+
<h1 className="text-xl font-bold text-neutral-900 mt-4 mb-2 first:mt-0">{children}</h1>
|
| 31 |
+
),
|
| 32 |
+
h2: ({ children }) => (
|
| 33 |
+
<h2 className="text-lg font-semibold text-neutral-800 mt-4 mb-2 first:mt-0">{children}</h2>
|
| 34 |
+
),
|
| 35 |
+
h3: ({ children }) => (
|
| 36 |
+
<h3 className="text-base font-semibold text-neutral-700 mt-3 mb-1.5 first:mt-0">{children}</h3>
|
| 37 |
+
),
|
| 38 |
+
ul: ({ children }) => (
|
| 39 |
+
<ul className="list-disc list-outside pl-5 mb-3 space-y-1 text-sm text-neutral-800">{children}</ul>
|
| 40 |
+
),
|
| 41 |
+
ol: ({ children }) => (
|
| 42 |
+
<ol className="list-decimal list-outside pl-5 mb-3 space-y-1 text-sm text-neutral-800">{children}</ol>
|
| 43 |
+
),
|
| 44 |
+
li: ({ children }) => <li className="leading-relaxed">{children}</li>,
|
| 45 |
+
code: ({ children, className }) => {
|
| 46 |
+
const isBlock = className?.startsWith("language-");
|
| 47 |
+
const language = className?.replace("language-", "") ?? "";
|
| 48 |
+
if (isBlock) {
|
| 49 |
+
return (
|
| 50 |
+
<div className="my-3 rounded-xl overflow-hidden border border-neutral-200">
|
| 51 |
+
{language && (
|
| 52 |
+
<div className="bg-neutral-50 border-b border-neutral-200 px-4 py-2">
|
| 53 |
+
<span className="text-xs font-mono text-neutral-500">{language}</span>
|
| 54 |
+
</div>
|
| 55 |
+
)}
|
| 56 |
+
<div className="p-4 bg-neutral-50 overflow-x-auto">
|
| 57 |
+
<code className="text-xs font-mono leading-relaxed text-neutral-800 whitespace-pre">
|
| 58 |
+
{children}
|
| 59 |
+
</code>
|
| 60 |
+
</div>
|
| 61 |
+
</div>
|
| 62 |
+
);
|
| 63 |
+
}
|
| 64 |
+
return (
|
| 65 |
+
<code className="px-1.5 py-0.5 rounded-md bg-neutral-100 text-brand-green font-mono text-xs">
|
| 66 |
+
{children}
|
| 67 |
+
</code>
|
| 68 |
+
);
|
| 69 |
+
},
|
| 70 |
+
pre: ({ children }) => <>{children}</>,
|
| 71 |
+
blockquote: ({ children }) => (
|
| 72 |
+
<blockquote className="border-l-4 border-brand-green pl-4 py-1 my-3 text-sm text-neutral-600 italic bg-brand-green-50 rounded-r-xl">
|
| 73 |
+
{children}
|
| 74 |
+
</blockquote>
|
| 75 |
+
),
|
| 76 |
+
a: ({ children, href }) => (
|
| 77 |
+
<a
|
| 78 |
+
href={href}
|
| 79 |
+
target="_blank"
|
| 80 |
+
rel="noopener noreferrer"
|
| 81 |
+
className="text-brand-green underline underline-offset-2 hover:text-brand-green/80 transition-colors"
|
| 82 |
+
>
|
| 83 |
+
{children}
|
| 84 |
+
</a>
|
| 85 |
+
),
|
| 86 |
+
strong: ({ children }) => <strong className="font-semibold">{children}</strong>,
|
| 87 |
+
hr: () => <hr className="border-neutral-200 my-3" />,
|
| 88 |
+
table: ({ children }) => (
|
| 89 |
+
<div className="my-3 overflow-x-auto rounded-xl border border-neutral-200 shadow-sm">
|
| 90 |
+
<table className="w-full text-sm border-collapse">{children}</table>
|
| 91 |
+
</div>
|
| 92 |
+
),
|
| 93 |
+
thead: ({ children }) => (
|
| 94 |
+
<thead className="bg-neutral-50 border-b border-neutral-200">{children}</thead>
|
| 95 |
+
),
|
| 96 |
+
th: ({ children }) => (
|
| 97 |
+
<th className="px-4 py-3 text-left text-xs font-semibold text-neutral-600 uppercase tracking-wider">
|
| 98 |
+
{children}
|
| 99 |
+
</th>
|
| 100 |
+
),
|
| 101 |
+
td: ({ children }) => (
|
| 102 |
+
<td className="px-4 py-3 text-sm text-neutral-800 border-b border-neutral-100">{children}</td>
|
| 103 |
+
),
|
| 104 |
+
tr: ({ children }) => (
|
| 105 |
+
<tr className="hover:bg-neutral-50 transition-colors">{children}</tr>
|
| 106 |
+
),
|
| 107 |
+
};
|
| 108 |
+
|
| 109 |
+
interface MarkdownRendererProps {
|
| 110 |
+
content: string;
|
| 111 |
+
skipPreprocess?: boolean;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
export default function MarkdownRenderer({ content, skipPreprocess }: MarkdownRendererProps) {
|
| 115 |
+
const processed = skipPreprocess ? content : preprocessMarkdown(content);
|
| 116 |
+
if (!skipPreprocess) {
|
| 117 |
+
const hasCR = content.includes("\r");
|
| 118 |
+
const hasDoubleNewline = content.includes("\n\n");
|
| 119 |
+
// console.log(
|
| 120 |
+
// `[MarkdownRenderer] skipPreprocess=false contentLen=${content.length} same=${content === processed} hasCR=${hasCR} hasDoubleNewline=${hasDoubleNewline}`
|
| 121 |
+
// );
|
| 122 |
+
// console.log("[MarkdownRenderer] CONTENT JSON →", JSON.stringify(content.slice(0, 600)));
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
return (
|
| 126 |
+
<ReactMarkdown
|
| 127 |
+
remarkPlugins={[remarkGfm, remarkMath]}
|
| 128 |
+
rehypePlugins={[rehypeKatex]}
|
| 129 |
+
components={components}
|
| 130 |
+
>
|
| 131 |
+
{processed}
|
| 132 |
+
</ReactMarkdown>
|
| 133 |
+
);
|
| 134 |
+
}
|
src/app/components/chat/types.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ChatSource } from "../../../services/api";
|
| 2 |
+
|
| 3 |
+
export interface Message {
|
| 4 |
+
id: string;
|
| 5 |
+
role: "user" | "assistant";
|
| 6 |
+
content: string;
|
| 7 |
+
audioText?: string;
|
| 8 |
+
timestamp: number;
|
| 9 |
+
sources?: ChatSource[];
|
| 10 |
+
/** PCM audio chunks from TTS — only populated when sent via voice mode */
|
| 11 |
+
audioChunks?: ArrayBuffer[];
|
| 12 |
+
audioSampleRate?: number;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export interface ChatSession {
|
| 16 |
+
id: string;
|
| 17 |
+
title: string;
|
| 18 |
+
createdAt: string;
|
| 19 |
+
updatedAt: string | null;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
export interface StoredUser {
|
| 23 |
+
user_id: string;
|
| 24 |
+
email: string;
|
| 25 |
+
name: string;
|
| 26 |
+
loginTime: string;
|
| 27 |
+
}
|
src/assets/logo.png
ADDED
|
src/assets/maintiva-logo.jpg
ADDED
|
src/audio/AudioPlayer.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const DEFAULT_SAMPLE_RATE = 16000;
|
| 2 |
+
|
| 3 |
+
export class AudioPlayer {
|
| 4 |
+
private context: AudioContext | null = null;
|
| 5 |
+
private nextPlayTime = 0;
|
| 6 |
+
private started = false;
|
| 7 |
+
private resumed = false;
|
| 8 |
+
private pendingBytes = 0;
|
| 9 |
+
private bufferThresholdBytes = 6400; // 200ms at 16kHz default
|
| 10 |
+
private pendingBuffers: AudioBuffer[] = [];
|
| 11 |
+
// Carries the leftover byte when a chunk has odd byte length, so PCM sample
|
| 12 |
+
// boundaries stay aligned across chunk splits from the HTTP stream.
|
| 13 |
+
private leftoverByte: number | null = null;
|
| 14 |
+
|
| 15 |
+
init(sampleRate = DEFAULT_SAMPLE_RATE): void {
|
| 16 |
+
if (this.context) {
|
| 17 |
+
if (this.context.sampleRate === sampleRate) return;
|
| 18 |
+
this.context.close();
|
| 19 |
+
this.context = null;
|
| 20 |
+
}
|
| 21 |
+
this.context = new AudioContext({ sampleRate });
|
| 22 |
+
this.bufferThresholdBytes = Math.floor(sampleRate * 0.5) * 2; // 500ms
|
| 23 |
+
this.nextPlayTime = 0;
|
| 24 |
+
this.started = false;
|
| 25 |
+
this.resumed = false;
|
| 26 |
+
this.pendingBytes = 0;
|
| 27 |
+
this.pendingBuffers = [];
|
| 28 |
+
this.leftoverByte = null;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
enqueue(rawPcm: ArrayBuffer, onStarted?: () => void): void {
|
| 32 |
+
if (!this.context) return;
|
| 33 |
+
|
| 34 |
+
// Prepend any leftover byte from the previous chunk so that Int16 sample
|
| 35 |
+
// boundaries are always aligned, regardless of how the HTTP stream splits.
|
| 36 |
+
let pcmBytes: Uint8Array;
|
| 37 |
+
if (this.leftoverByte !== null) {
|
| 38 |
+
const combined = new Uint8Array(1 + rawPcm.byteLength);
|
| 39 |
+
combined[0] = this.leftoverByte;
|
| 40 |
+
combined.set(new Uint8Array(rawPcm), 1);
|
| 41 |
+
pcmBytes = combined;
|
| 42 |
+
this.leftoverByte = null;
|
| 43 |
+
} else {
|
| 44 |
+
pcmBytes = new Uint8Array(rawPcm);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
// If still odd, save the trailing byte for the next chunk.
|
| 48 |
+
if (pcmBytes.byteLength % 2 !== 0) {
|
| 49 |
+
this.leftoverByte = pcmBytes[pcmBytes.byteLength - 1];
|
| 50 |
+
pcmBytes = pcmBytes.slice(0, pcmBytes.byteLength - 1);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
if (pcmBytes.byteLength === 0) return;
|
| 54 |
+
|
| 55 |
+
const int16 = new Int16Array(pcmBytes.buffer, pcmBytes.byteOffset, pcmBytes.byteLength / 2);
|
| 56 |
+
const float32 = new Float32Array(int16.length);
|
| 57 |
+
for (let i = 0; i < int16.length; i++) {
|
| 58 |
+
float32[i] = int16[i] / 32768;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
const audioBuffer = this.context.createBuffer(1, float32.length, this.context.sampleRate);
|
| 62 |
+
audioBuffer.copyToChannel(float32, 0);
|
| 63 |
+
|
| 64 |
+
this.pendingBytes += rawPcm.byteLength;
|
| 65 |
+
|
| 66 |
+
if (this.resumed) {
|
| 67 |
+
// AudioContext is running — schedule immediately
|
| 68 |
+
const source = this.context.createBufferSource();
|
| 69 |
+
source.buffer = audioBuffer;
|
| 70 |
+
source.connect(this.context.destination);
|
| 71 |
+
source.start(this.nextPlayTime);
|
| 72 |
+
this.nextPlayTime += audioBuffer.duration;
|
| 73 |
+
} else {
|
| 74 |
+
// Still buffering: waiting for threshold or waiting for ctx.resume() to complete
|
| 75 |
+
this.pendingBuffers.push(audioBuffer);
|
| 76 |
+
|
| 77 |
+
if (!this.started && this.pendingBytes >= this.bufferThresholdBytes) {
|
| 78 |
+
this.started = true;
|
| 79 |
+
void this.context.resume().then(() => {
|
| 80 |
+
if (!this.context) return;
|
| 81 |
+
this.resumed = true;
|
| 82 |
+
this.nextPlayTime = this.context.currentTime;
|
| 83 |
+
onStarted?.();
|
| 84 |
+
const toSchedule = this.pendingBuffers.splice(0);
|
| 85 |
+
this.pendingBuffers = [];
|
| 86 |
+
for (const buf of toSchedule) {
|
| 87 |
+
const source = this.context.createBufferSource();
|
| 88 |
+
source.buffer = buf;
|
| 89 |
+
source.connect(this.context.destination);
|
| 90 |
+
source.start(this.nextPlayTime);
|
| 91 |
+
this.nextPlayTime += buf.duration;
|
| 92 |
+
}
|
| 93 |
+
});
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
// Call after the stream ends to play any buffered audio that hasn't started yet
|
| 99 |
+
// (handles responses shorter than the buffer threshold)
|
| 100 |
+
flush(onStarted?: () => void): void {
|
| 101 |
+
if (!this.context || this.started || this.pendingBuffers.length === 0) return;
|
| 102 |
+
this.started = true;
|
| 103 |
+
void this.context.resume().then(() => {
|
| 104 |
+
if (!this.context) return;
|
| 105 |
+
this.resumed = true;
|
| 106 |
+
this.nextPlayTime = this.context.currentTime;
|
| 107 |
+
onStarted?.();
|
| 108 |
+
const toSchedule = this.pendingBuffers.splice(0);
|
| 109 |
+
this.pendingBuffers = [];
|
| 110 |
+
for (const buf of toSchedule) {
|
| 111 |
+
const source = this.context.createBufferSource();
|
| 112 |
+
source.buffer = buf;
|
| 113 |
+
source.connect(this.context.destination);
|
| 114 |
+
source.start(this.nextPlayTime);
|
| 115 |
+
this.nextPlayTime += buf.duration;
|
| 116 |
+
}
|
| 117 |
+
});
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
drain(): void {
|
| 121 |
+
// Let queued buffers play out — nothing to do, the AudioContext schedule handles it
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
stopImmediately(): void {
|
| 125 |
+
if (!this.context) return;
|
| 126 |
+
this.context.close();
|
| 127 |
+
this.context = null;
|
| 128 |
+
this.nextPlayTime = 0;
|
| 129 |
+
this.started = false;
|
| 130 |
+
this.resumed = false;
|
| 131 |
+
this.pendingBytes = 0;
|
| 132 |
+
this.pendingBuffers = [];
|
| 133 |
+
this.leftoverByte = null;
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
export function replayAudio(
|
| 138 |
+
chunks: ArrayBuffer[],
|
| 139 |
+
sampleRate: number
|
| 140 |
+
): () => void {
|
| 141 |
+
const ctx = new AudioContext({ sampleRate });
|
| 142 |
+
let nextTime = ctx.currentTime + 0.05;
|
| 143 |
+
|
| 144 |
+
for (const chunk of chunks) {
|
| 145 |
+
const int16 = new Int16Array(chunk, 0, Math.floor(chunk.byteLength / 2));
|
| 146 |
+
const float32 = new Float32Array(int16.length);
|
| 147 |
+
for (let i = 0; i < int16.length; i++) float32[i] = int16[i] / 32768;
|
| 148 |
+
const buf = ctx.createBuffer(1, float32.length, sampleRate);
|
| 149 |
+
buf.copyToChannel(float32, 0);
|
| 150 |
+
const src = ctx.createBufferSource();
|
| 151 |
+
src.buffer = buf;
|
| 152 |
+
src.connect(ctx.destination);
|
| 153 |
+
src.start(nextTime);
|
| 154 |
+
nextTime += buf.duration;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
return () => ctx.close();
|
| 158 |
+
}
|
src/audio/AudioRecorder.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const TARGET_SAMPLE_RATE = 16000;
|
| 2 |
+
const CHUNK_SAMPLES = 3200; // 100ms at 16kHz
|
| 3 |
+
|
| 4 |
+
export class AudioRecorder {
|
| 5 |
+
private context: AudioContext | null = null;
|
| 6 |
+
private stream: MediaStream | null = null;
|
| 7 |
+
private workletNode: AudioWorkletNode | null = null;
|
| 8 |
+
private accumulator: Float32Array = new Float32Array(0);
|
| 9 |
+
micLevel = 0;
|
| 10 |
+
|
| 11 |
+
async start(onChunk: (pcm: ArrayBuffer) => void): Promise<void> {
|
| 12 |
+
this.stream = await navigator.mediaDevices.getUserMedia({
|
| 13 |
+
audio: {
|
| 14 |
+
channelCount: 1,
|
| 15 |
+
echoCancellation: true,
|
| 16 |
+
noiseSuppression: true,
|
| 17 |
+
},
|
| 18 |
+
});
|
| 19 |
+
|
| 20 |
+
this.context = new AudioContext({ sampleRate: TARGET_SAMPLE_RATE });
|
| 21 |
+
await this.context.resume();
|
| 22 |
+
|
| 23 |
+
await this.context.audioWorklet.addModule(
|
| 24 |
+
new URL("./RecorderWorkletProcessor.js", import.meta.url).href
|
| 25 |
+
);
|
| 26 |
+
|
| 27 |
+
const source = this.context.createMediaStreamSource(this.stream);
|
| 28 |
+
this.workletNode = new AudioWorkletNode(this.context, "recorder-processor");
|
| 29 |
+
|
| 30 |
+
this.workletNode.port.onmessage = (e: MessageEvent<Float32Array>) => {
|
| 31 |
+
const input = e.data;
|
| 32 |
+
const combined = new Float32Array(this.accumulator.length + input.length);
|
| 33 |
+
combined.set(this.accumulator);
|
| 34 |
+
combined.set(input, this.accumulator.length);
|
| 35 |
+
this.accumulator = combined;
|
| 36 |
+
|
| 37 |
+
while (this.accumulator.length >= CHUNK_SAMPLES) {
|
| 38 |
+
const chunk = this.accumulator.slice(0, CHUNK_SAMPLES);
|
| 39 |
+
this.accumulator = this.accumulator.slice(CHUNK_SAMPLES);
|
| 40 |
+
|
| 41 |
+
// RMS for barge-in detection
|
| 42 |
+
let sumSq = 0;
|
| 43 |
+
for (let i = 0; i < chunk.length; i++) sumSq += chunk[i] * chunk[i];
|
| 44 |
+
this.micLevel = Math.sqrt(sumSq / chunk.length) * 32767;
|
| 45 |
+
|
| 46 |
+
// Convert float32 → int16 PCM
|
| 47 |
+
const int16 = new Int16Array(chunk.length);
|
| 48 |
+
for (let i = 0; i < chunk.length; i++) {
|
| 49 |
+
const s = Math.max(-1, Math.min(1, chunk[i]));
|
| 50 |
+
int16[i] = s < 0 ? s * 32768 : s * 32767;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
onChunk(int16.buffer);
|
| 54 |
+
}
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
+
source.connect(this.workletNode);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
stop(): void {
|
| 61 |
+
this.workletNode?.disconnect();
|
| 62 |
+
this.workletNode = null;
|
| 63 |
+
this.stream?.getTracks().forEach((t) => t.stop());
|
| 64 |
+
this.stream = null;
|
| 65 |
+
this.context?.close();
|
| 66 |
+
this.context = null;
|
| 67 |
+
this.accumulator = new Float32Array(0);
|
| 68 |
+
this.micLevel = 0;
|
| 69 |
+
}
|
| 70 |
+
}
|
src/audio/RecorderWorkletProcessor.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class RecorderProcessor extends AudioWorkletProcessor {
|
| 2 |
+
process(inputs) {
|
| 3 |
+
const channel = inputs[0]?.[0];
|
| 4 |
+
if (channel?.length) {
|
| 5 |
+
const copy = channel.slice();
|
| 6 |
+
this.port.postMessage(copy, [copy.buffer]);
|
| 7 |
+
}
|
| 8 |
+
return true;
|
| 9 |
+
}
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
registerProcessor("recorder-processor", RecorderProcessor);
|
src/hooks/useVoiceSession.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useRef, useEffect, useCallback } from "react";
|
| 2 |
+
import { AudioRecorder } from "../audio/AudioRecorder";
|
| 3 |
+
import { AudioPlayer } from "../audio/AudioPlayer";
|
| 4 |
+
import { createWavBlob, speechToText } from "../services/voiceApi";
|
| 5 |
+
|
| 6 |
+
export type VoiceState =
|
| 7 |
+
| "IDLE"
|
| 8 |
+
| "CONNECTING"
|
| 9 |
+
| "LISTENING"
|
| 10 |
+
| "PROCESSING"
|
| 11 |
+
| "SPEAKING"
|
| 12 |
+
| "ERROR";
|
| 13 |
+
|
| 14 |
+
export interface VoiceSessionParams {
|
| 15 |
+
sttProvider?: string;
|
| 16 |
+
ttsProvider?: string;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
interface UseVoiceSessionOptions {
|
| 20 |
+
onTranscript: (text: string) => void;
|
| 21 |
+
onError?: (code: string, message: string) => void;
|
| 22 |
+
sessionParams?: VoiceSessionParams;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export interface UseVoiceSessionReturn {
|
| 26 |
+
voiceState: VoiceState;
|
| 27 |
+
start: () => Promise<void>;
|
| 28 |
+
stop: () => void;
|
| 29 |
+
stopRecording: () => void;
|
| 30 |
+
setStateExternal: (s: VoiceState) => void;
|
| 31 |
+
isActive: boolean;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
const BUFFER_SOUNDS = [
|
| 35 |
+
"/sounds/01_Baik_Saya_sedang_memproses_pertanyaanmu.wav",
|
| 36 |
+
"/sounds/02_Oke_mohon_ditunggu_saya_sedang_siapkan_P.wav",
|
| 37 |
+
"/sounds/03_Sip_saya_terima_Sedang_saya_proses_Pesan.wav",
|
| 38 |
+
];
|
| 39 |
+
|
| 40 |
+
const RECORDER_SAMPLE_RATE = 16000;
|
| 41 |
+
|
| 42 |
+
function getVoiceHttpBaseUrl(): string {
|
| 43 |
+
const w = window as unknown as { __APP_CONFIG__?: { VOICE_API_URL?: string } };
|
| 44 |
+
return (
|
| 45 |
+
w.__APP_CONFIG__?.VOICE_API_URL ||
|
| 46 |
+
(import.meta as unknown as { env: Record<string, string> }).env.VITE_API_BASE_VOICE_URL ||
|
| 47 |
+
"http://localhost:7861"
|
| 48 |
+
);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
export function useVoiceSession(opts: UseVoiceSessionOptions): UseVoiceSessionReturn {
|
| 52 |
+
const [voiceState, setVoiceState] = useState<VoiceState>("IDLE");
|
| 53 |
+
const stateRef = useRef<VoiceState>("IDLE");
|
| 54 |
+
|
| 55 |
+
const recorderRef = useRef<AudioRecorder | null>(null);
|
| 56 |
+
const playerRef = useRef<AudioPlayer | null>(null);
|
| 57 |
+
const chunksRef = useRef<ArrayBuffer[]>([]);
|
| 58 |
+
|
| 59 |
+
const bufferAudioRef = useRef<HTMLAudioElement | null>(null);
|
| 60 |
+
const lastBufferIndexRef = useRef<number>(-1);
|
| 61 |
+
|
| 62 |
+
const optsRef = useRef(opts);
|
| 63 |
+
useEffect(() => { optsRef.current = opts; });
|
| 64 |
+
|
| 65 |
+
// Defined before setState so setState can call it without circular deps.
|
| 66 |
+
const stopBufferSound = useCallback(() => {
|
| 67 |
+
if (bufferAudioRef.current) {
|
| 68 |
+
bufferAudioRef.current.pause();
|
| 69 |
+
bufferAudioRef.current.currentTime = 0;
|
| 70 |
+
bufferAudioRef.current = null;
|
| 71 |
+
}
|
| 72 |
+
}, []);
|
| 73 |
+
|
| 74 |
+
// Auto-stops the buffer audio when the waiting phase ends (TTS about to start, or session ends).
|
| 75 |
+
const setState = useCallback((s: VoiceState) => {
|
| 76 |
+
if (s === "SPEAKING" || s === "IDLE" || s === "ERROR") {
|
| 77 |
+
stopBufferSound();
|
| 78 |
+
}
|
| 79 |
+
stateRef.current = s;
|
| 80 |
+
setVoiceState(s);
|
| 81 |
+
}, [stopBufferSound]);
|
| 82 |
+
|
| 83 |
+
const playBufferSound = useCallback(() => {
|
| 84 |
+
stopBufferSound();
|
| 85 |
+
let idx: number;
|
| 86 |
+
do {
|
| 87 |
+
idx = Math.floor(Math.random() * BUFFER_SOUNDS.length);
|
| 88 |
+
} while (BUFFER_SOUNDS.length > 1 && idx === lastBufferIndexRef.current);
|
| 89 |
+
lastBufferIndexRef.current = idx;
|
| 90 |
+
const audio = new Audio(BUFFER_SOUNDS[idx]);
|
| 91 |
+
bufferAudioRef.current = audio;
|
| 92 |
+
audio.play().catch(() => {});
|
| 93 |
+
}, [stopBufferSound]);
|
| 94 |
+
|
| 95 |
+
const stopSession = useCallback(() => {
|
| 96 |
+
recorderRef.current?.stop();
|
| 97 |
+
playerRef.current?.stopImmediately();
|
| 98 |
+
chunksRef.current = [];
|
| 99 |
+
setState("IDLE"); // setState("IDLE") calls stopBufferSound internally
|
| 100 |
+
}, [setState]);
|
| 101 |
+
|
| 102 |
+
const stopRecording = useCallback(() => {
|
| 103 |
+
if (stateRef.current !== "LISTENING") return;
|
| 104 |
+
setState("PROCESSING");
|
| 105 |
+
recorderRef.current?.stop();
|
| 106 |
+
// Play buffer audio — it keeps playing through STT and chatbot processing.
|
| 107 |
+
// It stops automatically when setState("SPEAKING"), setState("IDLE"), or setState("ERROR") is called.
|
| 108 |
+
playBufferSound();
|
| 109 |
+
|
| 110 |
+
const chunks = chunksRef.current;
|
| 111 |
+
chunksRef.current = [];
|
| 112 |
+
|
| 113 |
+
void (async () => {
|
| 114 |
+
try {
|
| 115 |
+
if (chunks.length === 0) {
|
| 116 |
+
setState("IDLE");
|
| 117 |
+
return;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
const wav = createWavBlob(chunks, RECORDER_SAMPLE_RATE);
|
| 121 |
+
const { text } = await speechToText(wav, optsRef.current.sessionParams?.sttProvider ?? "chirp3");
|
| 122 |
+
|
| 123 |
+
// Guard: session may have been cancelled while STT was in flight.
|
| 124 |
+
if (stateRef.current !== "PROCESSING") return;
|
| 125 |
+
|
| 126 |
+
if (text.trim()) {
|
| 127 |
+
console.log("[Voice] STT transcript →", text);
|
| 128 |
+
// Buffer audio continues to play while Main.tsx calls the chatbot API.
|
| 129 |
+
// It will stop when setStateExternal("SPEAKING") or setStateExternal("IDLE") is called.
|
| 130 |
+
optsRef.current.onTranscript(text);
|
| 131 |
+
} else {
|
| 132 |
+
setState("IDLE");
|
| 133 |
+
}
|
| 134 |
+
} catch (err) {
|
| 135 |
+
console.error("[STT] Request failed:", err);
|
| 136 |
+
optsRef.current.onError?.("STT_ERROR", (err as Error).message);
|
| 137 |
+
setState("ERROR"); // setState("ERROR") calls stopBufferSound internally
|
| 138 |
+
}
|
| 139 |
+
})();
|
| 140 |
+
}, [playBufferSound, setState]);
|
| 141 |
+
|
| 142 |
+
const start = useCallback(async () => {
|
| 143 |
+
if (stateRef.current !== "IDLE" && stateRef.current !== "ERROR") return;
|
| 144 |
+
setState("CONNECTING");
|
| 145 |
+
|
| 146 |
+
try {
|
| 147 |
+
const res = await fetch(`${getVoiceHttpBaseUrl()}/health`);
|
| 148 |
+
if (res.ok) {
|
| 149 |
+
const data: { status: string; message?: string } = await res.json();
|
| 150 |
+
if (data.status !== "ok") {
|
| 151 |
+
setState("ERROR");
|
| 152 |
+
optsRef.current.onError?.("HEALTH_CHECK_FAILED", data.message ?? "Service not ready");
|
| 153 |
+
return;
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
} catch {
|
| 157 |
+
// Network error or CORS — proceed with connect attempt
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
try {
|
| 161 |
+
chunksRef.current = [];
|
| 162 |
+
if (!recorderRef.current) recorderRef.current = new AudioRecorder();
|
| 163 |
+
if (!playerRef.current) playerRef.current = new AudioPlayer();
|
| 164 |
+
|
| 165 |
+
await recorderRef.current.start((chunk: ArrayBuffer) => {
|
| 166 |
+
if (stateRef.current === "LISTENING") {
|
| 167 |
+
chunksRef.current.push(chunk);
|
| 168 |
+
}
|
| 169 |
+
});
|
| 170 |
+
|
| 171 |
+
setState("LISTENING");
|
| 172 |
+
} catch (err) {
|
| 173 |
+
console.error("[Voice] Failed to start recorder:", err);
|
| 174 |
+
recorderRef.current?.stop();
|
| 175 |
+
optsRef.current.onError?.("MIC_ERROR", (err as Error).message ?? "Failed to access microphone");
|
| 176 |
+
setState("ERROR");
|
| 177 |
+
}
|
| 178 |
+
}, [setState]);
|
| 179 |
+
|
| 180 |
+
useEffect(() => {
|
| 181 |
+
return () => {
|
| 182 |
+
stopSession();
|
| 183 |
+
};
|
| 184 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 185 |
+
}, []);
|
| 186 |
+
|
| 187 |
+
return {
|
| 188 |
+
voiceState,
|
| 189 |
+
start,
|
| 190 |
+
stop: stopSession,
|
| 191 |
+
stopRecording,
|
| 192 |
+
setStateExternal: setState,
|
| 193 |
+
isActive: voiceState !== "IDLE" && voiceState !== "ERROR",
|
| 194 |
+
};
|
| 195 |
+
}
|
src/services/api.ts
CHANGED
|
@@ -40,6 +40,7 @@ export interface RoomMessage {
|
|
| 40 |
id: string;
|
| 41 |
role: "user" | "assistant";
|
| 42 |
content: string;
|
|
|
|
| 43 |
created_at: string;
|
| 44 |
sources?: ChatSource[];
|
| 45 |
}
|
|
@@ -48,7 +49,7 @@ export interface RoomDetail extends Room {
|
|
| 48 |
messages: RoomMessage[];
|
| 49 |
}
|
| 50 |
|
| 51 |
-
export type DocumentStatus = "
|
| 52 |
|
| 53 |
export interface ApiDocument {
|
| 54 |
id: string;
|
|
@@ -65,6 +66,13 @@ export interface UploadDocumentResponse {
|
|
| 65 |
data: { id: string; filename: string; status: DocumentStatus };
|
| 66 |
}
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
// ─── Base Client ──────────────────────────────────────────────────────────────
|
| 69 |
|
| 70 |
const BASE_URL = ((import.meta as unknown as { env: Record<string, string> }).env.VITE_API_BASE_URL) ?? "";
|
|
@@ -151,6 +159,87 @@ export const deleteDocument = (userId: string, documentId: string) =>
|
|
| 151 |
{ method: "DELETE" }
|
| 152 |
);
|
| 153 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
// ─── Chat ─────────────────────────────────────────────────────────────────────
|
| 155 |
|
| 156 |
export const streamChat = (
|
|
|
|
| 40 |
id: string;
|
| 41 |
role: "user" | "assistant";
|
| 42 |
content: string;
|
| 43 |
+
audio_text?: string | null;
|
| 44 |
created_at: string;
|
| 45 |
sources?: ChatSource[];
|
| 46 |
}
|
|
|
|
| 49 |
messages: RoomMessage[];
|
| 50 |
}
|
| 51 |
|
| 52 |
+
export type DocumentStatus = "uploaded" | "processing" | "completed" | "failed";
|
| 53 |
|
| 54 |
export interface ApiDocument {
|
| 55 |
id: string;
|
|
|
|
| 66 |
data: { id: string; filename: string; status: DocumentStatus };
|
| 67 |
}
|
| 68 |
|
| 69 |
+
export interface DocTypeInfo {
|
| 70 |
+
doc_type: string;
|
| 71 |
+
max_size: number;
|
| 72 |
+
status: "active" | "inactive";
|
| 73 |
+
message: string | null;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
// ─── Base Client ──────────────────────────────────────────────────────────────
|
| 77 |
|
| 78 |
const BASE_URL = ((import.meta as unknown as { env: Record<string, string> }).env.VITE_API_BASE_URL) ?? "";
|
|
|
|
| 159 |
{ method: "DELETE" }
|
| 160 |
);
|
| 161 |
|
| 162 |
+
export const getDocumentTypes = (): Promise<DocTypeInfo[]> =>
|
| 163 |
+
request<{ status: string; data: DocTypeInfo[] }>("/api/v1/documents/doctypes").then(
|
| 164 |
+
(res) => res.data
|
| 165 |
+
);
|
| 166 |
+
|
| 167 |
+
// ─── Database Clients ─────────────────────────────────────────────────────────
|
| 168 |
+
|
| 169 |
+
export type DbType = "postgres" | "mysql" | "sqlserver" | "supabase" | "bigquery" | "snowflake";
|
| 170 |
+
|
| 171 |
+
export interface DbTypeField {
|
| 172 |
+
name: string;
|
| 173 |
+
type: "string" | "integer" | "select" | "boolean";
|
| 174 |
+
required: boolean;
|
| 175 |
+
default: string | number | boolean | null;
|
| 176 |
+
description: string;
|
| 177 |
+
options?: string[];
|
| 178 |
+
sensitive?: boolean;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
export interface DbTypeInfo {
|
| 182 |
+
db_type: DbType;
|
| 183 |
+
display_name: string;
|
| 184 |
+
logo: string;
|
| 185 |
+
status: "active" | "inactive";
|
| 186 |
+
message: string | null;
|
| 187 |
+
fields: DbTypeField[];
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
export interface DatabaseClient {
|
| 191 |
+
id: string;
|
| 192 |
+
user_id: string;
|
| 193 |
+
name: string;
|
| 194 |
+
db_type: DbType;
|
| 195 |
+
status: "active" | "inactive";
|
| 196 |
+
created_at: string;
|
| 197 |
+
updated_at: string | null;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
export interface IngestResponse {
|
| 201 |
+
status: string;
|
| 202 |
+
client_id: string;
|
| 203 |
+
chunks_ingested: number;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
export const getDatabaseClientTypes = (): Promise<DbTypeInfo[]> =>
|
| 207 |
+
request<DbTypeInfo[]>("/api/v1/database-clients/dbtypes");
|
| 208 |
+
|
| 209 |
+
export const connectDatabase = (
|
| 210 |
+
userId: string,
|
| 211 |
+
dbType: DbType,
|
| 212 |
+
name: string,
|
| 213 |
+
credentials: Record<string, string | number | boolean>
|
| 214 |
+
): Promise<DatabaseClient> =>
|
| 215 |
+
request<DatabaseClient>(`/api/v1/database-clients?user_id=${userId}`, {
|
| 216 |
+
method: "POST",
|
| 217 |
+
body: JSON.stringify({ name, db_type: dbType, credentials }),
|
| 218 |
+
});
|
| 219 |
+
|
| 220 |
+
export const getDatabaseClients = (userId: string): Promise<DatabaseClient[]> =>
|
| 221 |
+
request<DatabaseClient[]>(`/api/v1/database-clients/${userId}`);
|
| 222 |
+
|
| 223 |
+
export const deleteDatabaseClient = (clientId: string, userId: string) =>
|
| 224 |
+
request<{ status: string; message: string }>(
|
| 225 |
+
`/api/v1/database-clients/${clientId}?user_id=${userId}`,
|
| 226 |
+
{ method: "DELETE" }
|
| 227 |
+
);
|
| 228 |
+
|
| 229 |
+
export const ingestDatabaseClient = (clientId: string, userId: string): Promise<IngestResponse> =>
|
| 230 |
+
request<IngestResponse>(
|
| 231 |
+
`/api/v1/database-clients/${clientId}/ingest?user_id=${userId}`,
|
| 232 |
+
{ method: "POST" }
|
| 233 |
+
);
|
| 234 |
+
|
| 235 |
+
// ─── Knowledge (Admin) ────────────────────────────────────────────────────────
|
| 236 |
+
|
| 237 |
+
export const rebuildKnowledge = (userId: string) =>
|
| 238 |
+
request<{ status: string; message: string }>(
|
| 239 |
+
`/api/v1/knowledge/rebuild?user_id=${userId}`,
|
| 240 |
+
{ method: "POST" }
|
| 241 |
+
);
|
| 242 |
+
|
| 243 |
// ─── Chat ─────────────────────────────────────────────────────────────────────
|
| 244 |
|
| 245 |
export const streamChat = (
|
src/services/voiceApi.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
function getVoiceBaseUrl(): string {
|
| 2 |
+
const w = window as unknown as { __APP_CONFIG__?: { VOICE_API_URL?: string } };
|
| 3 |
+
return (
|
| 4 |
+
w.__APP_CONFIG__?.VOICE_API_URL ||
|
| 5 |
+
(import.meta as unknown as { env: Record<string, string> }).env.VITE_API_BASE_VOICE_URL ||
|
| 6 |
+
"http://localhost:7861"
|
| 7 |
+
);
|
| 8 |
+
}
|
| 9 |
+
const VOICE_BASE_URL = getVoiceBaseUrl();
|
| 10 |
+
|
| 11 |
+
function writeString(view: DataView, offset: number, str: string): void {
|
| 12 |
+
for (let i = 0; i < str.length; i++) view.setUint8(offset + i, str.charCodeAt(i));
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export function createWavBlob(chunks: ArrayBuffer[], sampleRate: number): Blob {
|
| 16 |
+
const pcmByteLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);
|
| 17 |
+
const buffer = new ArrayBuffer(44 + pcmByteLength);
|
| 18 |
+
const view = new DataView(buffer);
|
| 19 |
+
writeString(view, 0, "RIFF");
|
| 20 |
+
view.setUint32(4, 36 + pcmByteLength, true);
|
| 21 |
+
writeString(view, 8, "WAVE");
|
| 22 |
+
writeString(view, 12, "fmt ");
|
| 23 |
+
view.setUint32(16, 16, true);
|
| 24 |
+
view.setUint16(20, 1, true); // PCM
|
| 25 |
+
view.setUint16(22, 1, true); // mono
|
| 26 |
+
view.setUint32(24, sampleRate, true);
|
| 27 |
+
view.setUint32(28, sampleRate * 2, true);
|
| 28 |
+
view.setUint16(32, 2, true);
|
| 29 |
+
view.setUint16(34, 16, true);
|
| 30 |
+
writeString(view, 36, "data");
|
| 31 |
+
view.setUint32(40, pcmByteLength, true);
|
| 32 |
+
let offset = 44;
|
| 33 |
+
for (const chunk of chunks) {
|
| 34 |
+
new Uint8Array(buffer, offset, chunk.byteLength).set(new Uint8Array(chunk));
|
| 35 |
+
offset += chunk.byteLength;
|
| 36 |
+
}
|
| 37 |
+
return new Blob([buffer], { type: "audio/wav" });
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
export async function speechToText(
|
| 41 |
+
wavBlob: Blob,
|
| 42 |
+
provider = "chirp3"
|
| 43 |
+
): Promise<{ text: string; language: string; duration: number | null }> {
|
| 44 |
+
const form = new FormData();
|
| 45 |
+
form.append("audio", wavBlob, "recording.wav");
|
| 46 |
+
form.append("provider", provider);
|
| 47 |
+
const res = await fetch(`${VOICE_BASE_URL}/stt`, { method: "POST", body: form });
|
| 48 |
+
if (!res.ok) throw new Error(`STT error: ${res.status}`);
|
| 49 |
+
const contentType = res.headers.get("content-type") ?? "";
|
| 50 |
+
if (!contentType.includes("application/json")) {
|
| 51 |
+
const body = await res.text();
|
| 52 |
+
throw new Error(`STT returned non-JSON (${res.status}): ${body.slice(0, 200)}`);
|
| 53 |
+
}
|
| 54 |
+
return res.json();
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
export async function textToSpeechStreaming(
|
| 58 |
+
text: string,
|
| 59 |
+
provider = "gemini"
|
| 60 |
+
): Promise<{ sampleRate: number; stream: ReadableStream<Uint8Array> }> {
|
| 61 |
+
const abort = new AbortController();
|
| 62 |
+
const timer = setTimeout(() => abort.abort(), 120_000);
|
| 63 |
+
const res = await fetch(`${VOICE_BASE_URL}/tts`, {
|
| 64 |
+
method: "POST",
|
| 65 |
+
headers: { "Content-Type": "application/json" },
|
| 66 |
+
body: JSON.stringify({ text, provider }),
|
| 67 |
+
signal: abort.signal,
|
| 68 |
+
}).finally(() => clearTimeout(timer));
|
| 69 |
+
if (!res.ok) throw new Error(`TTS error: ${res.status}`);
|
| 70 |
+
if (!res.body) throw new Error("TTS response has no body");
|
| 71 |
+
const sampleRate = parseInt(res.headers.get("X-Sample-Rate") ?? "24000", 10);
|
| 72 |
+
return { sampleRate, stream: res.body };
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
export async function textToSpeech(
|
| 76 |
+
text: string,
|
| 77 |
+
provider = "gemini"
|
| 78 |
+
): Promise<{ pcm: ArrayBuffer; sampleRate: number }> {
|
| 79 |
+
const abort = new AbortController();
|
| 80 |
+
const timer = setTimeout(() => abort.abort(), 90_000);
|
| 81 |
+
const response = await fetch(`${VOICE_BASE_URL}/tts`, {
|
| 82 |
+
method: "POST",
|
| 83 |
+
headers: { "Content-Type": "application/json" },
|
| 84 |
+
body: JSON.stringify({ text, provider }),
|
| 85 |
+
signal: abort.signal,
|
| 86 |
+
}).finally(() => clearTimeout(timer));
|
| 87 |
+
if (!response.ok) throw new Error(`TTS error: ${response.status}`);
|
| 88 |
+
const sampleRate = parseInt(
|
| 89 |
+
response.headers.get("X-Sample-Rate") ?? "24000",
|
| 90 |
+
10
|
| 91 |
+
);
|
| 92 |
+
const pcm = await response.arrayBuffer();
|
| 93 |
+
return { pcm, sampleRate };
|
| 94 |
+
}
|
src/styles/theme.css
CHANGED
|
@@ -2,6 +2,8 @@
|
|
| 2 |
|
| 3 |
:root {
|
| 4 |
--font-size: 16px;
|
|
|
|
|
|
|
| 5 |
--background: #ffffff;
|
| 6 |
--foreground: oklch(0.145 0 0);
|
| 7 |
--card: #ffffff;
|
|
@@ -117,6 +119,12 @@
|
|
| 117 |
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
| 118 |
--color-sidebar-border: var(--sidebar-border);
|
| 119 |
--color-sidebar-ring: var(--sidebar-ring);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
}
|
| 121 |
|
| 122 |
@layer base {
|
|
@@ -126,8 +134,16 @@
|
|
| 126 |
|
| 127 |
body {
|
| 128 |
@apply bg-background text-foreground;
|
|
|
|
|
|
|
|
|
|
| 129 |
}
|
| 130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
/**
|
| 132 |
* Default typography styles for HTML elements (h1-h4, p, label, button, input).
|
| 133 |
* These are in @layer base, so Tailwind utility classes (like text-sm, text-lg) automatically override them.
|
|
@@ -135,6 +151,7 @@
|
|
| 135 |
|
| 136 |
html {
|
| 137 |
font-size: var(--font-size);
|
|
|
|
| 138 |
}
|
| 139 |
|
| 140 |
h1 {
|
|
@@ -179,3 +196,21 @@
|
|
| 179 |
line-height: 1.5;
|
| 180 |
}
|
| 181 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
:root {
|
| 4 |
--font-size: 16px;
|
| 5 |
+
--brand-green: #18AF4A;
|
| 6 |
+
--brand-green-light: #1EC75A;
|
| 7 |
--background: #ffffff;
|
| 8 |
--foreground: oklch(0.145 0 0);
|
| 9 |
--card: #ffffff;
|
|
|
|
| 119 |
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
| 120 |
--color-sidebar-border: var(--sidebar-border);
|
| 121 |
--color-sidebar-ring: var(--sidebar-ring);
|
| 122 |
+
--color-brand-green: var(--brand-green);
|
| 123 |
+
--color-brand-green-light: var(--brand-green-light);
|
| 124 |
+
--color-brand-green-50: #F0FDF5;
|
| 125 |
+
--color-brand-green-100: #BBF7D0;
|
| 126 |
+
--color-brand-amber: #F5A623;
|
| 127 |
+
--color-brand-cyan: #48C8E8;
|
| 128 |
}
|
| 129 |
|
| 130 |
@layer base {
|
|
|
|
| 134 |
|
| 135 |
body {
|
| 136 |
@apply bg-background text-foreground;
|
| 137 |
+
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
| 138 |
+
-webkit-font-smoothing: antialiased;
|
| 139 |
+
-moz-osx-font-smoothing: grayscale;
|
| 140 |
}
|
| 141 |
|
| 142 |
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
| 143 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 144 |
+
::-webkit-scrollbar-thumb { background: #D1D5DB; border-radius: 3px; }
|
| 145 |
+
::-webkit-scrollbar-thumb:hover { background: #9CA3AF; }
|
| 146 |
+
|
| 147 |
/**
|
| 148 |
* Default typography styles for HTML elements (h1-h4, p, label, button, input).
|
| 149 |
* These are in @layer base, so Tailwind utility classes (like text-sm, text-lg) automatically override them.
|
|
|
|
| 151 |
|
| 152 |
html {
|
| 153 |
font-size: var(--font-size);
|
| 154 |
+
scroll-behavior: smooth;
|
| 155 |
}
|
| 156 |
|
| 157 |
h1 {
|
|
|
|
| 196 |
line-height: 1.5;
|
| 197 |
}
|
| 198 |
}
|
| 199 |
+
|
| 200 |
+
@keyframes float {
|
| 201 |
+
0%, 100% { transform: translateY(0px) rotate(var(--float-rotate, 0deg)); }
|
| 202 |
+
50% { transform: translateY(-14px) rotate(var(--float-rotate, 0deg)); }
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
@keyframes float-slow {
|
| 206 |
+
0%, 100% { transform: translateY(0px); }
|
| 207 |
+
50% { transform: translateY(-8px); }
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.animate-float {
|
| 211 |
+
animation: float 7s ease-in-out infinite;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
.animate-float-slow {
|
| 215 |
+
animation: float-slow 10s ease-in-out infinite;
|
| 216 |
+
}
|
src/vite-env.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/// <reference types="vite/client" />
|
| 2 |
+
|
| 3 |
+
declare module "*.jpg" {
|
| 4 |
+
const src: string;
|
| 5 |
+
export default src;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
declare module "*.jpeg" {
|
| 9 |
+
const src: string;
|
| 10 |
+
export default src;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
declare module "*.png" {
|
| 14 |
+
const src: string;
|
| 15 |
+
export default src;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
declare module "*.svg" {
|
| 19 |
+
const src: string;
|
| 20 |
+
export default src;
|
| 21 |
+
}
|
tsconfig.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2020",
|
| 4 |
+
"useDefineForClassFields": true,
|
| 5 |
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
| 6 |
+
"module": "ESNext",
|
| 7 |
+
"skipLibCheck": true,
|
| 8 |
+
"moduleResolution": "bundler",
|
| 9 |
+
"allowImportingTsExtensions": true,
|
| 10 |
+
"resolveJsonModule": true,
|
| 11 |
+
"isolatedModules": true,
|
| 12 |
+
"noEmit": true,
|
| 13 |
+
"jsx": "react-jsx",
|
| 14 |
+
"strict": true,
|
| 15 |
+
"noUnusedLocals": false,
|
| 16 |
+
"noUnusedParameters": false,
|
| 17 |
+
"noFallthroughCasesInSwitch": true,
|
| 18 |
+
"baseUrl": ".",
|
| 19 |
+
"paths": {
|
| 20 |
+
"@/*": ["./src/*"]
|
| 21 |
+
}
|
| 22 |
+
},
|
| 23 |
+
"include": ["src"]
|
| 24 |
+
}
|