init
17
vw-agentic-rag/web/.env
Normal file
@@ -0,0 +1,17 @@
|
||||
# Next.js Environment Variables
|
||||
# These will be used as fallback values when K8s environment variables are not set
|
||||
# URL prefix for external routing (used by Ingress)
|
||||
NEXT_PUBLIC_API_URL_PREFIX=/agentic-rag
|
||||
|
||||
# API URL for backend communication
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8000/api
|
||||
|
||||
# # LangGraph API URL
|
||||
# LANGGRAPH_API_URL=http://localhost:8000
|
||||
|
||||
NEXT_PUBLIC_LANGGRAPH_ASSISTANT_ID=default
|
||||
|
||||
# Disable Next.js telemetry
|
||||
NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
|
||||
8
vw-agentic-rag/web/.env-bak
Normal file
@@ -0,0 +1,8 @@
|
||||
# LangGraph Configuration (for development)
|
||||
NEXT_PUBLIC_LANGGRAPH_API_URL=http://localhost:8000/api
|
||||
NEXT_PUBLIC_LANGGRAPH_ASSISTANT_ID=default
|
||||
|
||||
# For production with LangSmith/LangGraph Cloud (commented out for local dev)
|
||||
# LANGCHAIN_API_KEY=your_api_key
|
||||
# LANGGRAPH_API_URL=your_production_api_url
|
||||
# NEXT_PUBLIC_LANGGRAPH_API_URL=your_production_api_url
|
||||
40
vw-agentic-rag/web/.gitignore
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
36
vw-agentic-rag/web/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
16
vw-agentic-rag/web/eslint.config.mjs
Normal file
@@ -0,0 +1,16 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
24
vw-agentic-rag/web/next.config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'standalone',
|
||||
basePath: '/agentic-rag',
|
||||
assetPrefix: '/agentic-rag',
|
||||
images: {
|
||||
unoptimized: true, // 禁用图片优化,使用直接路径
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/webchat',
|
||||
destination: '/',
|
||||
},
|
||||
{
|
||||
source: '/webchat/:path*',
|
||||
destination: '/:path*',
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
43
vw-agentic-rag/web/package.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"upgrade": "npx assistant-ui upgrade"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^0.0.72",
|
||||
"@assistant-ui/react": "^0.10.43",
|
||||
"@assistant-ui/react-data-stream": "^0.10.1",
|
||||
"@assistant-ui/react-langgraph": "^0.5.12",
|
||||
"@assistant-ui/react-markdown": "^0.10.9",
|
||||
"@assistant-ui/react-ui": "^0.1.8",
|
||||
"@langchain/langgraph-sdk": "^0.0.109",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"isomorphic-dompurify": "^2.26.0",
|
||||
"next": "15.4.7",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"rehype-external-links": "^3.0.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.4.7",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "3",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
6492
vw-agentic-rag/web/pnpm-lock.yaml
generated
Normal file
8
vw-agentic-rag/web/postcss.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
1
vw-agentic-rag/web/public/CATOnline.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="_图层_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60"><defs><style>.cls-1{fill:#bdf906;}</style></defs><path class="cls-1" d="M30.07-.02C13.49-.02,0,13.47,0,30.05s13.49,30.07,30.07,30.07,30.07-13.49,30.07-30.07S46.65-.02,30.07-.02ZM30.07,50.06c-11.03,0-20.01-8.98-20.01-20.01S19.04,10.05,30.07,10.05s20.01,8.98,20.01,20.01-8.98,20.01-20.01,20.01Z"/><rect class="cls-1" x="18.24" y="20.24" width="7.65" height="11.15" rx="3.31" ry="3.31"/><rect class="cls-1" x="35" y="20.24" width="7.65" height="11.15" rx="3.31" ry="3.31"/></svg>
|
||||
|
After Width: | Height: | Size: 589 B |
31
vw-agentic-rag/web/public/debug-tool-ui.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 前端工具UI调试脚本
|
||||
* 在浏览器控制台中运行以检查工具UI注册状态
|
||||
*/
|
||||
|
||||
// 检查 assistant-ui 工具UI注册状态
|
||||
console.log("🔍 Checking Assistant UI Tool Registration...");
|
||||
|
||||
// 检查是否有工具UI组件
|
||||
console.log("AssistantRuntimeProvider:", typeof window.AssistantRuntimeProvider);
|
||||
console.log("makeAssistantToolUI:", typeof window.makeAssistantToolUI);
|
||||
|
||||
// 检查页面上的组件
|
||||
const toolUIElements = document.querySelectorAll('[data-tool-ui]');
|
||||
console.log(`Found ${toolUIElements.length} tool UI elements:`, toolUIElements);
|
||||
|
||||
// 检查React组件树中的工具UI
|
||||
if (window.React && window.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED) {
|
||||
console.log("React DevTools available - can inspect component tree");
|
||||
}
|
||||
|
||||
// 监听网络请求
|
||||
const originalFetch = window.fetch;
|
||||
window.fetch = function(...args) {
|
||||
if (args[0].includes('/api/chat')) {
|
||||
console.log("🌐 Chat API request:", args);
|
||||
}
|
||||
return originalFetch.apply(this, args);
|
||||
};
|
||||
|
||||
console.log("✅ Tool UI debugging setup complete");
|
||||
452
vw-agentic-rag/web/public/embed.min.js
vendored
Normal file
@@ -0,0 +1,452 @@
|
||||
(function () {
|
||||
// Constants for DOM element IDs and configuration key
|
||||
const configKey = "agenticragChatbotConfig";
|
||||
const buttonId = "agenticrag-chatbot-bubble-button";
|
||||
const iframeId = "agenticrag-chatbot-bubble-window";
|
||||
const config = window[configKey];
|
||||
let isExpanded = false;
|
||||
|
||||
let svgIcons='';
|
||||
if(config.openButtonIconUrl) {
|
||||
const openIcon = `<img src="${config.openButtonIconUrl}" id='openIcon' width='24' height='24' />`;
|
||||
svgIcons = openIcon;
|
||||
}else{
|
||||
// SVG icons for open and close states
|
||||
svgIcons = `<svg id="openIcon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.7586 2L16.2412 2C17.0462 1.99999 17.7105 1.99998 18.2517 2.04419C18.8138 2.09012 19.3305 2.18868 19.8159 2.43598C20.5685 2.81947 21.1804 3.43139 21.5639 4.18404C21.8112 4.66937 21.9098 5.18608 21.9557 5.74818C21.9999 6.28937 21.9999 6.95373 21.9999 7.7587L22 14.1376C22.0004 14.933 22.0007 15.5236 21.8636 16.0353C21.4937 17.4156 20.4155 18.4938 19.0352 18.8637C18.7277 18.9461 18.3917 18.9789 17.9999 18.9918L17.9999 20.371C18 20.6062 18 20.846 17.9822 21.0425C17.9651 21.2305 17.9199 21.5852 17.6722 21.8955C17.3872 22.2525 16.9551 22.4602 16.4983 22.4597C16.1013 22.4593 15.7961 22.273 15.6386 22.1689C15.474 22.06 15.2868 21.9102 15.1031 21.7632L12.69 19.8327C12.1714 19.4178 12.0174 19.3007 11.8575 19.219C11.697 19.137 11.5262 19.0771 11.3496 19.0408C11.1737 19.0047 10.9803 19 10.3162 19H7.75858C6.95362 19 6.28927 19 5.74808 18.9558C5.18598 18.9099 4.66928 18.8113 4.18394 18.564C3.43129 18.1805 2.81937 17.5686 2.43588 16.816C2.18859 16.3306 2.09002 15.8139 2.0441 15.2518C1.99988 14.7106 1.99989 14.0463 1.9999 13.2413V7.75868C1.99989 6.95372 1.99988 6.28936 2.0441 5.74818C2.09002 5.18608 2.18859 4.66937 2.43588 4.18404C2.81937 3.43139 3.43129 2.81947 4.18394 2.43598C4.66928 2.18868 5.18598 2.09012 5.74808 2.04419C6.28927 1.99998 6.95364 1.99999 7.7586 2ZM10.5073 7.5C10.5073 6.67157 9.83575 6 9.00732 6C8.1789 6 7.50732 6.67157 7.50732 7.5C7.50732 8.32843 8.1789 9 9.00732 9C9.83575 9 10.5073 8.32843 10.5073 7.5ZM16.6073 11.7001C16.1669 11.3697 15.5426 11.4577 15.2105 11.8959C15.1488 11.9746 15.081 12.0486 15.0119 12.1207C14.8646 12.2744 14.6432 12.4829 14.3566 12.6913C13.7796 13.111 12.9818 13.5001 12.0073 13.5001C11.0328 13.5001 10.235 13.111 9.65799 12.6913C9.37138 12.4829 9.15004 12.2744 9.00274 12.1207C8.93366 12.0486 8.86581 11.9745 8.80418 11.8959C8.472 11.4577 7.84775 11.3697 7.40732 11.7001C6.96549 12.0314 6.87595 12.6582 7.20732 13.1001C7.20479 13.0968 7.21072 13.1043 7.22094 13.1171C7.24532 13.1478 7.29407 13.2091 7.31068 13.2289C7.36932 13.2987 7.45232 13.3934 7.55877 13.5045C7.77084 13.7258 8.08075 14.0172 8.48165 14.3088C9.27958 14.8891 10.4818 15.5001 12.0073 15.5001C13.5328 15.5001 14.735 14.8891 15.533 14.3088C15.9339 14.0172 16.2438 13.7258 16.4559 13.5045C16.5623 13.3934 16.6453 13.2987 16.704 13.2289C16.7333 13.1939 16.7567 13.165 16.7739 13.1432C17.1193 12.6969 17.0729 12.0493 16.6073 11.7001ZM15.0073 6C15.8358 6 16.5073 6.67157 16.5073 7.5C16.5073 8.32843 15.8358 9 15.0073 9C14.1789 9 13.5073 8.32843 13.5073 7.5C13.5073 6.67157 14.1789 6 15.0073 6Z" fill="white"/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
svgIcons+=` <svg id="closeIcon" style="display:none" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 18L6 6M6 18L18 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>`;
|
||||
|
||||
const originalIframeStyleText = `
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
top: unset;
|
||||
right: var(--${buttonId}-right, 1rem); /* Align with agenticrag-chatbot-bubble-button. */
|
||||
bottom: var(--${buttonId}-bottom, 1rem); /* Align with agenticrag-chatbot-bubble-button. */
|
||||
left: unset;
|
||||
width: 24rem;
|
||||
max-width: calc(100vw - 2rem);
|
||||
height: 43.75rem;
|
||||
max-height: calc(100vh - 6rem);
|
||||
border: none;
|
||||
border-radius: 1rem;
|
||||
z-index: 2147483640;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
transition-property: width, height;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
`
|
||||
|
||||
const expandedIframeStyleText = `
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
top: unset;
|
||||
right: var(--${buttonId}-right, 1rem); /* Align with agenticrag-chatbot-bubble-button. */
|
||||
bottom: var(--${buttonId}-bottom, 1rem); /* Align with agenticrag-chatbot-bubble-button. */
|
||||
left: unset;
|
||||
min-width: 24rem;
|
||||
width: 48%;
|
||||
max-width: 40rem; /* Match mobile breakpoint*/
|
||||
min-height: 43.75rem;
|
||||
height: 88%;
|
||||
max-height: calc(100vh - 6rem);
|
||||
border: none;
|
||||
border-radius: 1rem;
|
||||
z-index: 2147483640;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
transition-property: width, height;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
`
|
||||
|
||||
// Main function to embed the chatbot
|
||||
async function embedChatbot() {
|
||||
let isDragging = false
|
||||
|
||||
if (!config || !config.token) {
|
||||
console.error(`${configKey} is empty or token is not provided`);
|
||||
return;
|
||||
}
|
||||
|
||||
async function compressAndEncodeBase64(input) {
|
||||
const uint8Array = new TextEncoder().encode(input);
|
||||
const compressedStream = new Response(
|
||||
new Blob([uint8Array])
|
||||
.stream()
|
||||
.pipeThrough(new CompressionStream("gzip"))
|
||||
).arrayBuffer();
|
||||
const compressedUint8Array = new Uint8Array(await compressedStream);
|
||||
return btoa(String.fromCharCode(...compressedUint8Array));
|
||||
}
|
||||
|
||||
async function getCompressedInputsFromConfig() {
|
||||
const inputs = config?.inputs || {};
|
||||
const compressedInputs = {};
|
||||
await Promise.all(
|
||||
Object.entries(inputs).map(async ([key, value]) => {
|
||||
compressedInputs[key] = await compressAndEncodeBase64(value);
|
||||
})
|
||||
);
|
||||
return compressedInputs;
|
||||
}
|
||||
|
||||
async function getCompressedSystemVariablesFromConfig() {
|
||||
const systemVariables = config?.systemVariables || {};
|
||||
const compressedSystemVariables = {};
|
||||
await Promise.all(
|
||||
Object.entries(systemVariables).map(async ([key, value]) => {
|
||||
compressedSystemVariables[`sys.${key}`] = await compressAndEncodeBase64(value);
|
||||
})
|
||||
);
|
||||
return compressedSystemVariables;
|
||||
}
|
||||
|
||||
async function getCompressedUserVariablesFromConfig() {
|
||||
const userVariables = config?.userVariables || {};
|
||||
const compressedUserVariables = {};
|
||||
await Promise.all(
|
||||
Object.entries(userVariables).map(async ([key, value]) => {
|
||||
compressedUserVariables[`user.${key}`] = await compressAndEncodeBase64(value);
|
||||
})
|
||||
);
|
||||
return compressedUserVariables;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
...await getCompressedInputsFromConfig(),
|
||||
...await getCompressedSystemVariablesFromConfig(),
|
||||
...await getCompressedUserVariablesFromConfig()
|
||||
});
|
||||
|
||||
const baseUrl = config.baseUrl || `https://${config.isDev ? "dev." : ""}uagenticrag.app`;
|
||||
const targetOrigin = new URL(baseUrl).origin;
|
||||
|
||||
// pre-check the length of the URL
|
||||
const iframeUrl = `${baseUrl}/chatbot/${config.token}?${params}`;
|
||||
// 1) CREATE the iframe immediately, so it can load in the background:
|
||||
const preloadedIframe = createIframe();
|
||||
// 2) HIDE it by default:
|
||||
preloadedIframe.style.display = "none";
|
||||
// 3) APPEND it to the document body right away:
|
||||
document.body.appendChild(preloadedIframe);
|
||||
// ─── End Fix Snippet
|
||||
if (iframeUrl.length > 2048) {
|
||||
console.error("The URL is too long, please reduce the number of inputs to prevent the bot from failing to load");
|
||||
}
|
||||
|
||||
// Function to create the iframe for the chatbot
|
||||
function createIframe() {
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.allow = "fullscreen;microphone";
|
||||
iframe.title = "agentic rag chatbot bubble window";
|
||||
iframe.id = iframeId;
|
||||
// iframe.src = iframeUrl;
|
||||
iframe.style.cssText = originalIframeStyleText;
|
||||
|
||||
return iframe;
|
||||
}
|
||||
|
||||
// Function to reset the iframe position
|
||||
function resetIframePosition() {
|
||||
if (window.innerWidth <= 640) return;
|
||||
|
||||
const targetIframe = document.getElementById(iframeId);
|
||||
const targetButton = document.getElementById(buttonId);
|
||||
if (targetIframe && targetButton) {
|
||||
const buttonRect = targetButton.getBoundingClientRect();
|
||||
// We don't necessarily need iframeRect anymore with the center logic
|
||||
|
||||
const viewportCenterY = window.innerHeight / 2;
|
||||
const buttonCenterY = buttonRect.top + buttonRect.height / 2;
|
||||
|
||||
if (buttonCenterY < viewportCenterY) {
|
||||
targetIframe.style.top = `var(--${buttonId}-bottom, 1rem)`;
|
||||
targetIframe.style.bottom = 'unset';
|
||||
} else {
|
||||
targetIframe.style.bottom = `var(--${buttonId}-bottom, 4.5rem)`;
|
||||
targetIframe.style.top = 'unset';
|
||||
}
|
||||
|
||||
const viewportCenterX = window.innerWidth / 2;
|
||||
const buttonCenterX = buttonRect.left + buttonRect.width / 2;
|
||||
|
||||
if (buttonCenterX < viewportCenterX) {
|
||||
targetIframe.style.left = `var(--${buttonId}-right, 1rem)`;
|
||||
targetIframe.style.right = 'unset';
|
||||
} else {
|
||||
targetIframe.style.right = `var(--${buttonId}-right, 1rem)`;
|
||||
targetIframe.style.left = 'unset';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleExpand() {
|
||||
isExpanded = !isExpanded;
|
||||
|
||||
const targetIframe = document.getElementById(iframeId);
|
||||
if (!targetIframe) return;
|
||||
|
||||
if (isExpanded) {
|
||||
targetIframe.style.cssText = expandedIframeStyleText;
|
||||
} else {
|
||||
targetIframe.style.cssText = originalIframeStyleText;
|
||||
}
|
||||
resetIframePosition();
|
||||
}
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.origin !== targetOrigin) return;
|
||||
|
||||
const targetIframe = document.getElementById(iframeId);
|
||||
if (!targetIframe || event.source !== targetIframe.contentWindow) return;
|
||||
|
||||
if (event.data.type === 'agenticrag-chatbot-iframe-ready') {
|
||||
targetIframe.contentWindow?.postMessage(
|
||||
{
|
||||
type: 'agenticrag-chatbot-config',
|
||||
payload: {
|
||||
isToggledByButton: true,
|
||||
isDraggable: !!config.draggable,
|
||||
},
|
||||
},
|
||||
targetOrigin
|
||||
);
|
||||
}
|
||||
|
||||
if (event.data.type === 'agenticrag-chatbot-expand-change') {
|
||||
toggleExpand();
|
||||
}
|
||||
});
|
||||
|
||||
// Function to create the chat button
|
||||
function createButton() {
|
||||
const containerDiv = document.createElement("div");
|
||||
// Apply custom properties from config
|
||||
Object.entries(config.containerProps || {}).forEach(([key, value]) => {
|
||||
if (key === "className") {
|
||||
containerDiv.classList.add(...value.split(" "));
|
||||
} else if (key === "style") {
|
||||
if (typeof value === "object") {
|
||||
Object.assign(containerDiv.style, value);
|
||||
} else {
|
||||
containerDiv.style.cssText = value;
|
||||
}
|
||||
} else if (typeof value === "function") {
|
||||
containerDiv.addEventListener(
|
||||
key.replace(/^on/, "").toLowerCase(),
|
||||
value
|
||||
);
|
||||
} else {
|
||||
containerDiv[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
containerDiv.id = buttonId;
|
||||
|
||||
// Add styles for the button
|
||||
const styleSheet = document.createElement("style");
|
||||
document.head.appendChild(styleSheet);
|
||||
styleSheet.sheet.insertRule(`
|
||||
#${containerDiv.id} {
|
||||
position: fixed;
|
||||
bottom: var(--${containerDiv.id}-bottom, 1rem);
|
||||
right: var(--${containerDiv.id}-right, 1rem);
|
||||
left: var(--${containerDiv.id}-left, unset);
|
||||
top: var(--${containerDiv.id}-top, unset);
|
||||
width: var(--${containerDiv.id}-width, 48px);
|
||||
height: var(--${containerDiv.id}-height, 48px);
|
||||
border-radius: var(--${containerDiv.id}-border-radius, 25px);
|
||||
background-color: var(--${containerDiv.id}-bg-color, #155EEF);
|
||||
box-shadow: var(--${containerDiv.id}-box-shadow, rgba(0, 0, 0, 0.2) 0px 4px 8px 0px);
|
||||
cursor: pointer;
|
||||
z-index: 2147483647;
|
||||
}
|
||||
`);
|
||||
|
||||
// Create display div for the button icon
|
||||
const displayDiv = document.createElement("div");
|
||||
displayDiv.style.cssText =
|
||||
"position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 2147483647;";
|
||||
displayDiv.innerHTML = svgIcons;
|
||||
containerDiv.appendChild(displayDiv);
|
||||
document.body.appendChild(containerDiv);
|
||||
|
||||
// Add click event listener to toggle chatbot
|
||||
containerDiv.addEventListener("click", handleClick);
|
||||
// Add touch event listener
|
||||
containerDiv.addEventListener("touchend", (event) => {
|
||||
event.preventDefault();
|
||||
handleClick();
|
||||
}, { passive: false });
|
||||
|
||||
function handleClick() {
|
||||
if (isDragging) return;
|
||||
|
||||
const targetIframe = document.getElementById(iframeId);
|
||||
if (!targetIframe) {
|
||||
containerDiv.appendChild(createIframe());
|
||||
resetIframePosition();
|
||||
this.title = "Exit (ESC)";
|
||||
setSvgIcon("close");
|
||||
document.addEventListener("keydown", handleEscKey);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
targetIframe.style.display = targetIframe.style.display === "none" ? "block" : "none";
|
||||
targetIframe.style.display === "none" ? setSvgIcon("open") : setSvgIcon("close");
|
||||
|
||||
if (targetIframe.style.display === "none") {
|
||||
targetIframe.src = '';
|
||||
document.removeEventListener("keydown", handleEscKey);
|
||||
} else {
|
||||
targetIframe.src = iframeUrl;
|
||||
document.addEventListener("keydown", handleEscKey);
|
||||
}
|
||||
|
||||
resetIframePosition();
|
||||
}
|
||||
|
||||
// Enable dragging if specified in config
|
||||
if (config.draggable) {
|
||||
enableDragging(containerDiv, config.dragAxis || "both");
|
||||
}
|
||||
}
|
||||
|
||||
// Function to enable dragging of the chat button
|
||||
function enableDragging(element, axis) {
|
||||
let startX, startY, startClientX, startClientY;
|
||||
|
||||
element.addEventListener("mousedown", startDragging);
|
||||
element.addEventListener("touchstart", startDragging);
|
||||
|
||||
function startDragging(e) {
|
||||
isDragging = false;
|
||||
if (e.type === "touchstart") {
|
||||
startX = e.touches[0].clientX - element.offsetLeft;
|
||||
startY = e.touches[0].clientY - element.offsetTop;
|
||||
startClientX = e.touches[0].clientX;
|
||||
startClientY = e.touches[0].clientY;
|
||||
} else {
|
||||
startX = e.clientX - element.offsetLeft;
|
||||
startY = e.clientY - element.offsetTop;
|
||||
startClientX = e.clientX;
|
||||
startClientY = e.clientY;
|
||||
}
|
||||
document.addEventListener("mousemove", drag);
|
||||
document.addEventListener("touchmove", drag, { passive: false });
|
||||
document.addEventListener("mouseup", stopDragging);
|
||||
document.addEventListener("touchend", stopDragging);
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function drag(e) {
|
||||
const touch = e.type === "touchmove" ? e.touches[0] : e;
|
||||
const deltaX = touch.clientX - startClientX;
|
||||
const deltaY = touch.clientY - startClientY;
|
||||
|
||||
// Determine whether it is a drag operation
|
||||
if (Math.abs(deltaX) > 8 || Math.abs(deltaY) > 8) {
|
||||
isDragging = true;
|
||||
}
|
||||
|
||||
if (!isDragging) return;
|
||||
|
||||
element.style.transition = "none";
|
||||
element.style.cursor = "grabbing";
|
||||
|
||||
// Hide iframe while dragging
|
||||
const targetIframe = document.getElementById(iframeId);
|
||||
if (targetIframe) {
|
||||
targetIframe.style.display = "none";
|
||||
setSvgIcon("open");
|
||||
}
|
||||
|
||||
let newLeft, newBottom;
|
||||
if (e.type === "touchmove") {
|
||||
newLeft = e.touches[0].clientX - startX;
|
||||
newBottom = window.innerHeight - e.touches[0].clientY - startY;
|
||||
} else {
|
||||
newLeft = e.clientX - startX;
|
||||
newBottom = window.innerHeight - e.clientY - startY;
|
||||
}
|
||||
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const maxX = window.innerWidth - elementRect.width;
|
||||
const maxY = window.innerHeight - elementRect.height;
|
||||
|
||||
// Update position based on drag axis
|
||||
if (axis === "x" || axis === "both") {
|
||||
element.style.setProperty(
|
||||
`--${buttonId}-left`,
|
||||
`${Math.max(0, Math.min(newLeft, maxX))}px`
|
||||
);
|
||||
}
|
||||
|
||||
if (axis === "y" || axis === "both") {
|
||||
element.style.setProperty(
|
||||
`--${buttonId}-bottom`,
|
||||
`${Math.max(0, Math.min(newBottom, maxY))}px`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function stopDragging() {
|
||||
setTimeout(() => {
|
||||
isDragging = false;
|
||||
}, 0);
|
||||
element.style.transition = "";
|
||||
element.style.cursor = "pointer";
|
||||
|
||||
document.removeEventListener("mousemove", drag);
|
||||
document.removeEventListener("touchmove", drag);
|
||||
document.removeEventListener("mouseup", stopDragging);
|
||||
document.removeEventListener("touchend", stopDragging);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the chat button if it doesn't exist
|
||||
if (!document.getElementById(buttonId)) {
|
||||
createButton();
|
||||
}
|
||||
}
|
||||
|
||||
function setSvgIcon(type = "open") {
|
||||
if (type === "open") {
|
||||
document.getElementById("openIcon").style.display = "block";
|
||||
document.getElementById("closeIcon").style.display = "none";
|
||||
} else {
|
||||
document.getElementById("openIcon").style.display = "none";
|
||||
document.getElementById("closeIcon").style.display = "block";
|
||||
}
|
||||
}
|
||||
|
||||
// Add esc Exit keyboard event triggered
|
||||
function handleEscKey(event) {
|
||||
if (event.key === "Escape") {
|
||||
const targetIframe = document.getElementById(iframeId);
|
||||
if (targetIframe && targetIframe.style.display !== "none") {
|
||||
targetIframe.style.display = "none";
|
||||
setSvgIcon("open");
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", handleEscKey);
|
||||
|
||||
// Set the embedChatbot function to run when the body is loaded,Avoid infinite nesting
|
||||
if (config?.dynamicScript) {
|
||||
embedChatbot();
|
||||
} else {
|
||||
document.body.onload = embedChatbot;
|
||||
}
|
||||
})();
|
||||
43
vw-agentic-rag/web/public/embeded-sample/sample-dev.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<!-- embeded AI Assistant sample scripts Begin-->
|
||||
<script>
|
||||
//set ui language in query string. support: zh-cn, en
|
||||
baseUrl = "http://aidemo.japaneast.cloudapp.azure.com/agentic-rag?language=zh-cn";
|
||||
openButtonIconUrl = "/agentic-rag/CATOnline.svg";
|
||||
window.agenticragChatbotConfig = {
|
||||
token: Array(32)
|
||||
.fill(0)
|
||||
.map(() => Math.random().toString(36).charAt(2))
|
||||
.join(""),
|
||||
baseUrl: baseUrl,
|
||||
systemVariables: {},
|
||||
openButtonIconUrl: openButtonIconUrl
|
||||
};
|
||||
</script>
|
||||
<script src="http://aidemo.japaneast.cloudapp.azure.com/agentic-rag/embed.min.js" defer></script>
|
||||
<style>
|
||||
#agenticrag-chatbot-bubble-button {
|
||||
background-color: #1c64f2 !important;
|
||||
}
|
||||
|
||||
#agenticrag-chatbot-bubble-window {
|
||||
width: 40rem !important;
|
||||
height: 56rem !important;
|
||||
}
|
||||
</style>
|
||||
<!-- embeded AI Assistant sample scripts End-->
|
||||
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(300deg, gray 100%, lightgray 0%);
|
||||
/* min-height: 100vh; */
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
</html>
|
||||
44
vw-agentic-rag/web/public/embeded-sample/sample-prd.html
Normal file
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<!-- embeded AI Assistant sample scripts Begin-->
|
||||
<script>
|
||||
//set ui language in query string. support: zh-cn, en
|
||||
baseUrl = "https://ai.cdp.vgcserv.com.cn/agentic-rag?language=zh-cn";
|
||||
|
||||
openButtonIconUrl = "/agentic-rag/CATOnline.svg";
|
||||
window.agenticragChatbotConfig = {
|
||||
token: Array(32)
|
||||
.fill(0)
|
||||
.map(() => Math.random().toString(36).charAt(2))
|
||||
.join(""),
|
||||
baseUrl: baseUrl,
|
||||
systemVariables: {},
|
||||
openButtonIconUrl: openButtonIconUrl
|
||||
};
|
||||
</script>
|
||||
<script src="https://ai.cdp.vgcserv.com.cn/agentic-rag/embed.min.js" defer></script>
|
||||
<style>
|
||||
#agenticrag-chatbot-bubble-button {
|
||||
background-color: #1c64f2 !important;
|
||||
}
|
||||
|
||||
#agenticrag-chatbot-bubble-window {
|
||||
width: 40rem !important;
|
||||
height: 56rem !important;
|
||||
}
|
||||
</style>
|
||||
<!-- embeded AI Assistant sample scripts End-->
|
||||
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(300deg, gray 100%, lightgray 0%);
|
||||
/* min-height: 100vh; */
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
</html>
|
||||
44
vw-agentic-rag/web/public/embeded-sample/sample.html
Normal file
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<!-- embeded AI Assistant sample scripts Begin-->
|
||||
<script>
|
||||
//set ui language in query string. support: zh-cn, en
|
||||
baseUrl = "http://localhost:3000/agentic-rag?language=zh-cn";
|
||||
// set open button icon url
|
||||
openButtonIconUrl = "/agentic-rag/CATOnline.svg";
|
||||
window.agenticragChatbotConfig = {
|
||||
token: Array(32)
|
||||
.fill(0)
|
||||
.map(() => Math.random().toString(36).charAt(2))
|
||||
.join(""),
|
||||
baseUrl: baseUrl,
|
||||
systemVariables: {},
|
||||
openButtonIconUrl: openButtonIconUrl
|
||||
};
|
||||
</script>
|
||||
<script src="http://localhost:3000/agentic-rag/embed.min.js" defer></script>
|
||||
<style>
|
||||
#agenticrag-chatbot-bubble-button {
|
||||
background-color: #1c64f2 !important;
|
||||
}
|
||||
|
||||
#agenticrag-chatbot-bubble-window {
|
||||
width: 40rem !important;
|
||||
height: 56rem !important;
|
||||
}
|
||||
</style>
|
||||
<!-- embeded AI Assistant sample scripts End-->
|
||||
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(300deg, gray 100%, lightgray 0%);
|
||||
/* min-height: 100vh; */
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
</html>
|
||||
1
vw-agentic-rag/web/public/file.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
1
vw-agentic-rag/web/public/globe.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
BIN
vw-agentic-rag/web/public/legal-document.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
vw-agentic-rag/web/public/legal.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
1
vw-agentic-rag/web/public/next.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
BIN
vw-agentic-rag/web/public/search.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
vw-agentic-rag/web/public/user-guide.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
1
vw-agentic-rag/web/public/vercel.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
1
vw-agentic-rag/web/public/window.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
89
vw-agentic-rag/web/src/app/api/chat/route.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
// Allow streaming responses up to 30 seconds
|
||||
export const maxDuration = 30;
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { messages, sessionId } = await req.json();
|
||||
|
||||
// Transform assistant-ui message format to backend format
|
||||
const transformedMessages = messages.map((msg: { role: string; content: unknown }) => ({
|
||||
role: msg.role,
|
||||
content: Array.isArray(msg.content)
|
||||
? msg.content.map((part: { text?: string; content?: string }) => part.text || part.content || '').join('')
|
||||
: msg.content
|
||||
}));
|
||||
|
||||
// Get session ID from multiple sources (priority order: body, header, generate new)
|
||||
const headerSessionId = req.headers.get('X-Session-ID');
|
||||
const effectiveSessionId = sessionId || headerSessionId || `session_${Date.now()}_${Math.random().toString(36).substring(2)}`;
|
||||
|
||||
console.log(`Using session ID: ${effectiveSessionId}`);
|
||||
|
||||
// Forward request to our Python backend with enhanced configuration
|
||||
|
||||
const apiUrl = process.env["NEXT_PUBLIC_API_URL"] || "http://localhost:8000/api";
|
||||
const backendResponse = await fetch(apiUrl+'/ai-sdk/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'text/plain',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: transformedMessages,
|
||||
session_id: effectiveSessionId,
|
||||
// Add metadata for better assistant-ui integration
|
||||
metadata: {
|
||||
source: 'assistant-ui',
|
||||
version: '0.10.x',
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!backendResponse.ok) {
|
||||
const errorText = await backendResponse.text();
|
||||
console.error(`Backend error (${backendResponse.status}):`, errorText);
|
||||
throw new Error(`Backend responded with status: ${backendResponse.status}`);
|
||||
}
|
||||
|
||||
// Return the stream from our backend with proper Data Stream Protocol headers
|
||||
return new Response(backendResponse.body, {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Connection': 'keep-alive',
|
||||
'x-vercel-ai-data-stream': 'v1', // AI SDK compatibility
|
||||
'x-assistant-ui-stream': 'v1', // assistant-ui compatibility
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Accept',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Chat API error:', error);
|
||||
|
||||
// Return error in Data Stream Protocol format
|
||||
return new Response(
|
||||
`3:${JSON.stringify(error instanceof Error ? error.message : 'Unknown error')}\n`,
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'x-vercel-ai-data-stream': 'v1',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle preflight requests
|
||||
export async function OPTIONS() {
|
||||
return new Response(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Accept',
|
||||
},
|
||||
});
|
||||
}
|
||||
8
vw-agentic-rag/web/src/app/api/config/route.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
apiUrlPrefix: process.env["NEXT_PUBLIC_API_URL_PREFIX"] || "",
|
||||
apiUrl: process.env["NEXT_PUBLIC_API_URL"] || "http://localhost:8000",
|
||||
});
|
||||
}
|
||||
9
vw-agentic-rag/web/src/app/api/health/route.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
service: 'agentic-rag-web'
|
||||
});
|
||||
}
|
||||
72
vw-agentic-rag/web/src/app/api/langgraph/[...path]/route.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
function getCorsHeaders() {
|
||||
return {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "*",
|
||||
};
|
||||
}
|
||||
|
||||
async function handleRequest(req: NextRequest, method: string) {
|
||||
try {
|
||||
const path = req.nextUrl.pathname.replace(/^\/?api\/langgraph\//, "");
|
||||
const url = new URL(req.url);
|
||||
const searchParams = new URLSearchParams(url.search);
|
||||
searchParams.delete("_path");
|
||||
searchParams.delete("nxtP_path");
|
||||
const queryString = searchParams.toString()
|
||||
? `?${searchParams.toString()}`
|
||||
: "";
|
||||
|
||||
const options: RequestInit = {
|
||||
method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
// Add auth header if needed for production
|
||||
// "x-api-key": process.env["LANGCHAIN_API_KEY"] || "",
|
||||
},
|
||||
};
|
||||
|
||||
if (["POST", "PUT", "PATCH"].includes(method)) {
|
||||
options.body = await req.text();
|
||||
}
|
||||
|
||||
// Forward to our FastAPI backend
|
||||
const backendUrl = process.env["LANGGRAPH_API_URL"] || "http://localhost:8000";
|
||||
const res = await fetch(
|
||||
`${backendUrl}/api/${path}${queryString}`,
|
||||
options,
|
||||
);
|
||||
|
||||
return new NextResponse(res.body, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
headers: {
|
||||
...res.headers,
|
||||
...getCorsHeaders(),
|
||||
},
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
const error = e as { message?: string; status?: number };
|
||||
return NextResponse.json({ error: error.message || 'Internal Server Error' }, { status: error.status ?? 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export const GET = (req: NextRequest) => handleRequest(req, "GET");
|
||||
export const POST = (req: NextRequest) => handleRequest(req, "POST");
|
||||
export const PUT = (req: NextRequest) => handleRequest(req, "PUT");
|
||||
export const PATCH = (req: NextRequest) => handleRequest(req, "PATCH");
|
||||
export const DELETE = (req: NextRequest) => handleRequest(req, "DELETE");
|
||||
|
||||
// Add a new OPTIONS handler
|
||||
export const OPTIONS = () => {
|
||||
return new NextResponse(null, {
|
||||
status: 204,
|
||||
headers: {
|
||||
...getCorsHeaders(),
|
||||
},
|
||||
});
|
||||
};
|
||||
BIN
vw-agentic-rag/web/src/app/favicon.ico
Normal file
|
After Width: | Height: | Size: 25 KiB |
326
vw-agentic-rag/web/src/app/globals.css
Normal file
@@ -0,0 +1,326 @@
|
||||
@import "@assistant-ui/react-ui/styles/index.css";
|
||||
@import "@assistant-ui/react-markdown/styles/dot.css";
|
||||
|
||||
/* Tailwind CSS 基础层 */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* 强制刷新缓存标记 - v1.0.1 */
|
||||
|
||||
/* 自定义动画 */
|
||||
@layer utilities {
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-slow {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-gentle {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.85;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin-slow {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fade-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
.animate-slide-in {
|
||||
animation: slide-in 0.4s ease-out;
|
||||
}
|
||||
|
||||
.animate-pulse-slow {
|
||||
animation: pulse-slow 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-pulse-gentle {
|
||||
animation: pulse-gentle 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-spin-slow {
|
||||
animation: spin-slow 3s linear infinite;
|
||||
}
|
||||
|
||||
/* 文本截断工具类 */
|
||||
.line-clamp-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* CSS变量定义 */
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 221.2 83.2% 53.3%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96%;
|
||||
--secondary-foreground: 222.2 84% 4.9%;
|
||||
--muted: 210 40% 96%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96%;
|
||||
--accent-foreground: 222.2 84% 4.9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 221.2 83.2% 53.3%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 84% 4.9%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 基础样式重置 */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 隐藏头像,给内容更多空间 */
|
||||
[data-assistant-message] [data-testid="avatar"],
|
||||
[data-user-message] [data-testid="avatar"],
|
||||
.aui-avatar,
|
||||
.aui-avatar-root,
|
||||
.aui-avatar-image,
|
||||
.aui-avatar-fallback {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 调整消息布局以移除头像空间 */
|
||||
[data-assistant-message],
|
||||
[data-user-message],
|
||||
.aui-message {
|
||||
margin-left: 0 !important;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
/* 增加助手回复内容区域上方的间距 */
|
||||
[data-assistant-message] .aui-message-content,
|
||||
[data-assistant-message] [data-testid="message-content"],
|
||||
.aui-assistant-message .aui-message-content {
|
||||
margin-top: 1.5rem !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 为助手消息区域添加背景色 */
|
||||
[data-assistant-message],
|
||||
.aui-assistant-message {
|
||||
background-color: hsl(var(--muted) / 0.3) !important;
|
||||
border-radius: 0.5rem !important;
|
||||
padding: 1rem !important;
|
||||
margin: 0.5rem 0 !important;
|
||||
}
|
||||
|
||||
/* 滚动条隐藏 */
|
||||
.aui-thread-viewport,
|
||||
.aui-thread-messages {
|
||||
scrollbar-width: none !important; /* Firefox */
|
||||
-ms-overflow-style: none !important; /* IE and Edge */
|
||||
}
|
||||
|
||||
.aui-thread-viewport::-webkit-scrollbar,
|
||||
.aui-thread-messages::-webkit-scrollbar {
|
||||
display: none !important; /* Chrome, Safari, Opera */
|
||||
}
|
||||
|
||||
/* 增加主内容区域的最大宽度 */
|
||||
.aui-thread {
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
/* Tool Call 状态颜色优化 */
|
||||
.tool-status-running {
|
||||
color: hsl(var(--primary) / 0.8);
|
||||
}
|
||||
|
||||
.tool-status-processing {
|
||||
color: hsl(45 93% 47% / 0.8); /* 温暖的琥珀色,透明度80% */
|
||||
}
|
||||
|
||||
.tool-status-complete {
|
||||
color: hsl(142 71% 45%); /* 更柔和的翠绿色 */
|
||||
}
|
||||
|
||||
.tool-status-error {
|
||||
color: hsl(var(--destructive) / 0.8);
|
||||
}
|
||||
|
||||
.aui-assistant-message-content{
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
.aui-thread-viewport{
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.prose {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.h-full.rounded-lg.border.bg-background{
|
||||
border:none !important;
|
||||
}
|
||||
|
||||
/* .prose * {
|
||||
margin-top: 1rem !important;
|
||||
margin-bottom: 1rem !important;
|
||||
}
|
||||
*/
|
||||
|
||||
/* .prose p + p,
|
||||
.prose p + ul,
|
||||
.prose p + ol,
|
||||
.prose ul + p,
|
||||
.prose ol + p,
|
||||
.prose li + li,
|
||||
.prose h1 + *,
|
||||
.prose h2 + *,
|
||||
.prose h3 + *,
|
||||
.prose h4 + * {
|
||||
margin-top: 0.75em !important;
|
||||
} */
|
||||
|
||||
/* 欢迎页示例问题样式 */
|
||||
.aui-thread-welcome-suggestions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
padding: 0 2rem;
|
||||
max-width: 1000px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.aui-thread-welcome-suggestion {
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid hsl(var(--border));
|
||||
background: hsl(var(--card));
|
||||
color: hsl(var(--card-foreground));
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
min-height: 4rem;
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
.aui-thread-welcome-suggestion:hover {
|
||||
background: hsl(var(--accent));
|
||||
border-color: hsl(var(--accent-foreground) / 0.2);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
.aui-thread-welcome-suggestion:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
/* 移动设备优化 */
|
||||
@media (max-width: 640px) {
|
||||
.aui-thread-welcome-suggestions {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
padding: 0 1rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.aui-thread-welcome-suggestion {
|
||||
padding: 1rem 1.25rem;
|
||||
font-size: 0.85rem;
|
||||
min-height: 3.5rem;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
34
vw-agentic-rag/web/src/app/layout.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "AI Agentic RAG",
|
||||
description: "",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
7
vw-agentic-rag/web/src/app/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Assistant } from "../components/Assistant";
|
||||
|
||||
export default function ChatPage() {
|
||||
return (<Assistant></Assistant> );
|
||||
}
|
||||
96
vw-agentic-rag/web/src/components/Assistant.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
|
||||
import { AssistantRuntimeProvider } from "@assistant-ui/react";
|
||||
import { useDataStreamRuntime } from "@assistant-ui/react-data-stream";
|
||||
|
||||
|
||||
import { MyChat } from "./ui/mychat";
|
||||
import {
|
||||
RetrieveStandardRegulationUI,
|
||||
RetrieveDocChunkStandardRegulationUI,
|
||||
RetrieveSystemUsermanualUI
|
||||
} from "./ToolUIs";
|
||||
import { useSessionId } from "../hooks/useSessionId";
|
||||
import { LanguageSwitcher } from "./LanguageSwitcher";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
|
||||
interface AssistantProps {
|
||||
welcomeMessage?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified Assistant component following assistant-ui best practices
|
||||
* Supports Data Stream Runtime with proper error handling and session management
|
||||
*/
|
||||
export function Assistant({
|
||||
welcomeMessage = "Hello! I'm your AI agent for manufacturing standards and regulations. How can I help you today?",
|
||||
className = ""
|
||||
}: AssistantProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const sessionId = useSessionId();
|
||||
const prefix = process.env.NEXT_PUBLIC_API_URL_PREFIX || '';
|
||||
|
||||
const runtime = useDataStreamRuntime({
|
||||
api: prefix + "/api/chat",
|
||||
// Pass session ID via headers
|
||||
headers: {
|
||||
'X-Session-ID': sessionId
|
||||
},
|
||||
onFinish: (message) => {
|
||||
console.log("sessionId:", sessionId);
|
||||
console.log("Complete message:", message);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Runtime error:", error);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<div className="h-screen flex flex-col">
|
||||
<header className="p-4 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">
|
||||
{t.appTitle}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t.appDescription}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<LanguageSwitcher />
|
||||
<button onClick={()=>{
|
||||
// runtime.thread.reset();
|
||||
window.location.reload();
|
||||
}} className="p-2 rounded-full hover:bg-[#eef2ff] text-[#475569] " aria-label="Toggle theme"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-refresh-cw w-4 h-4"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"></path><path d="M21 3v5h-5"></path><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"></path><path d="M8 16H3v5"></path></svg></button>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 max-w-5xl mx-auto w-full p-4">
|
||||
<div className="h-full rounded-lg border bg-background">
|
||||
<div className='h-full'>
|
||||
<AssistantRuntimeProvider runtime={runtime}>
|
||||
{/* Tool UI Registration */}
|
||||
<RetrieveStandardRegulationUI />
|
||||
<RetrieveDocChunkStandardRegulationUI />
|
||||
<RetrieveSystemUsermanualUI />
|
||||
{/* Main Thread */}
|
||||
<MyChat />
|
||||
</AssistantRuntimeProvider>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
);
|
||||
}
|
||||
63
vw-agentic-rag/web/src/components/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React, { Component, type ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Concise Error Boundary following DRY principles
|
||||
*/
|
||||
export class ErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
// Simplified error reporting
|
||||
console.error('UI Error:', error, errorInfo);
|
||||
|
||||
// Optional: Report to backend
|
||||
fetch('/api/error-report', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
}).catch(() => {}); // Silent fail for error reporting
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return this.props.fallback || (
|
||||
<div className="flex flex-col items-center justify-center min-h-[200px] p-6 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div className="text-4xl text-red-500 mb-4">⚠️</div>
|
||||
<h2 className="text-lg font-semibold text-red-700 mb-2">Something went wrong</h2>
|
||||
<p className="text-sm text-red-600 mb-4 text-center">
|
||||
The chat component encountered an error. Please refresh the page and try again.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => this.setState({ hasError: false, error: undefined })}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700"
|
||||
>
|
||||
🔄 Retry
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
49
vw-agentic-rag/web/src/components/ErrorToast.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
|
||||
interface ErrorData {
|
||||
id: string;
|
||||
message: string;
|
||||
type: 'error' | 'warning' | 'network';
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple error toast component
|
||||
*/
|
||||
export const ErrorToastComponent = ({ error, onClose }: {
|
||||
error: ErrorData;
|
||||
onClose: (id: string) => void;
|
||||
}) => {
|
||||
const getStyle = () => {
|
||||
switch (error.type) {
|
||||
case 'network': return 'bg-orange-100 border-orange-200 text-orange-800';
|
||||
case 'warning': return 'bg-yellow-100 border-yellow-200 text-yellow-800';
|
||||
default: return 'bg-red-100 border-red-200 text-red-800';
|
||||
}
|
||||
};
|
||||
|
||||
const getIcon = () => {
|
||||
switch (error.type) {
|
||||
case 'network': return '📶';
|
||||
case 'warning': return '⚠️';
|
||||
default: return '❌';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`fixed top-4 right-4 z-50 max-w-md p-4 border rounded-lg shadow-lg ${getStyle()}`}>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-lg">{getIcon()}</span>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">{error.message}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onClose(error.id)}
|
||||
className="opacity-70 hover:opacity-100 text-lg"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
30
vw-agentic-rag/web/src/components/LanguageSwitcher.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useTranslation } from "../hooks/useTranslation";
|
||||
import React from "react";
|
||||
import { Language } from "../utils/i18n";
|
||||
|
||||
export function LanguageSwitcher() {
|
||||
const { language, switchLanguage, availableLanguages } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">Language:</span>
|
||||
<div className="flex rounded-md border bg-background">
|
||||
{availableLanguages.map((lang: Language) => (
|
||||
<button
|
||||
key={lang}
|
||||
onClick={() => switchLanguage(lang)}
|
||||
className={`px-2 py-1 text-xs font-medium transition-colors ${
|
||||
language === lang
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||
} ${
|
||||
lang === 'zh' ? 'rounded-l' : 'rounded-r'
|
||||
}`}
|
||||
>
|
||||
{lang === 'zh' ? '中文' : 'EN'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
214
vw-agentic-rag/web/src/components/ToolUIs.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||
import { ToolCallContentPartProps } from "@assistant-ui/react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "../hooks/useTranslation";
|
||||
import { Translations } from "../utils/i18n";
|
||||
|
||||
import Image from "next/image";
|
||||
|
||||
interface ToolUIConfig {
|
||||
toolName: string;
|
||||
iconSrc: string;
|
||||
titleKey: keyof Pick<Translations, 'toolStandardSearch' | 'toolDocumentSearch' | 'toolSystemUserManual'>;
|
||||
}
|
||||
|
||||
// 工具UI渲染组件
|
||||
function ToolUIRenderer(props: ToolCallContentPartProps & {
|
||||
iconSrc: string;
|
||||
titleKey: keyof Pick<Translations, 'toolStandardSearch' | 'toolDocumentSearch' | 'toolSystemUserManual'>;
|
||||
}) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [showRaw, setShowRaw] = useState(false);
|
||||
const { language,t } = useTranslation();
|
||||
const isRunning = props.status.type === "running";
|
||||
const queryString = typeof props.args?.query === 'string' ? props.args.query : String(props.args?.query || '');
|
||||
const resultCount = Array.isArray(props.result) ? props.result.length : 0;
|
||||
const hasValidResult = props.result !== null && props.result !== undefined;
|
||||
|
||||
// 格式化检索结果显示
|
||||
const formatRetrievalResult = (result: unknown ) => {
|
||||
if (Array.isArray(result)) {
|
||||
return result.map((item, index) => {
|
||||
if (typeof item === 'object' && item !== null) {
|
||||
const doc = item as Record<string, unknown>;
|
||||
let title = typeof doc.title === 'string' ? doc.title : `文档 ${index + 1}`;
|
||||
const score = typeof doc.score === 'number' ? doc.score.toFixed(4) : String(doc.score || '');
|
||||
let content = typeof doc.content === 'string' ? doc.content : '';
|
||||
const document_code = typeof doc.document_code === 'string' ? doc.document_code : '';
|
||||
const document_category = typeof doc.document_category === 'string' ? doc.document_category : '';
|
||||
|
||||
if(props.titleKey=="toolStandardSearch"){
|
||||
content = document_code;
|
||||
if(document_category=="Standard"){
|
||||
const standard_title_cn = typeof doc.x_Standard_Title_CN === 'string' ? doc.x_Standard_Title_CN : '';
|
||||
const standard_title_en = typeof doc.x_Standard_Title_EN === 'string' ? doc.x_Standard_Title_EN : '';
|
||||
title= language=="zh"?standard_title_cn: standard_title_en ;
|
||||
}else if(document_category=="Regulation"){
|
||||
const regulation_title_cn = typeof doc.x_Regulation_Title_CN === 'string' ? doc.x_Regulation_Title_CN : '';
|
||||
const regulation_title_en = typeof doc.x_Regulation_Title_EN === 'string' ? doc.x_Regulation_Title_EN : '';
|
||||
title= language=="zh"?regulation_title_cn: regulation_title_en ;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={index} className="mb-3 p-3 bg-background border rounded-lg">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="font-medium text-sm text-foreground">
|
||||
{title}
|
||||
</div>
|
||||
{typeof doc.score === 'number' && (
|
||||
<span className="text-xs text-muted-foreground bg-muted px-2 py-1 rounded">
|
||||
评分: {score}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{content && (
|
||||
<div className="text-sm text-muted-foreground mb-2 line-clamp-3">
|
||||
{content.length > 200 ? `${content.substring(0, 200)}...` : content}
|
||||
</div>
|
||||
)}
|
||||
{typeof doc.metadata === 'object' && doc.metadata && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<strong>元数据:</strong> {JSON.stringify(doc.metadata, null, 1)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div key={index} className="mb-2 p-2 bg-muted rounded text-sm">
|
||||
{JSON.stringify(item, null, 2)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border bg-card text-card-foreground p-3 my-2 transition-all duration-200 ease-in-out hover:shadow-md">
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer group"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative w-5 h-5 flex-shrink-0">
|
||||
<Image
|
||||
src={props.iconSrc}
|
||||
alt={t[props.titleKey] as string}
|
||||
width={20}
|
||||
height={20}
|
||||
className={`transition-transform duration-200 ${isRunning ? 'animate-pulse-gentle' : ''}`}
|
||||
/>
|
||||
{isRunning && (
|
||||
<div className="absolute -top-0.5 -right-0.5 w-2 h-2 bg-blue-500 rounded-full animate-spin-slow">
|
||||
<div className="w-1 h-1 bg-white rounded-full absolute top-0.5 left-0.5"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm font-medium group-hover:text-primary transition-colors">
|
||||
{t[props.titleKey]}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{isRunning && (
|
||||
<div className="flex items-center justify-center space-x-2 text-amber-500">
|
||||
<svg className="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span> {t.statusSearching}</span>
|
||||
</div>
|
||||
|
||||
)}
|
||||
{props.status.type === "complete" && resultCount > 0 && (
|
||||
<span className="bg-cyan-100 text-cyan-800 text-sm font-medium px-2.5 py-0.5 rounded inline-block">
|
||||
{t.toolFound} {resultCount} {t.toolResults}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground transition-transform duration-200 group-hover:translate-x-1">
|
||||
{isExpanded ? "▲" : "▼"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="mt-3 pt-3 border-t border-border animate-fade-in">
|
||||
{props.args && (
|
||||
<div className="mb-3">
|
||||
<div className="text-xs text-muted-foreground mb-1">{t.toolQuery}:</div>
|
||||
<div className="text-sm bg-muted rounded p-2 break-words">
|
||||
|
||||
{queryString.length > 100 ? `${queryString.substring(0, 100)}...` : queryString}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.status.type === "complete" && hasValidResult && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t.toolResults} ({resultCount}):
|
||||
</div>
|
||||
{Array.isArray(props.result) && (
|
||||
<button
|
||||
onClick={() => setShowRaw(!showRaw)}
|
||||
className="text-xs text-blue-600 hover:text-blue-800 transition-colors"
|
||||
>
|
||||
{showRaw ? "格式化显示" : "原始数据"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-h-96 overflow-auto">
|
||||
{Array.isArray(props.result) && !showRaw ? (
|
||||
formatRetrievalResult(props.result)
|
||||
) : (
|
||||
<div className="text-xs bg-muted rounded p-2">
|
||||
<pre className="whitespace-pre-wrap font-mono">
|
||||
{JSON.stringify(props.result, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 通用工具UI组件生成器
|
||||
function createToolUI({ toolName, iconSrc, titleKey }: ToolUIConfig) {
|
||||
return makeAssistantToolUI({
|
||||
toolName,
|
||||
render: (props) => (
|
||||
<ToolUIRenderer
|
||||
{...props}
|
||||
iconSrc={iconSrc}
|
||||
titleKey={titleKey}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// 工具UI实例
|
||||
export const RetrieveStandardRegulationUI = createToolUI({
|
||||
toolName: "retrieve_standard_regulation",
|
||||
iconSrc: `${process.env.NEXT_PUBLIC_API_URL_PREFIX || ''}/legal-document.png`,
|
||||
titleKey: "toolStandardSearch"
|
||||
});
|
||||
|
||||
export const RetrieveDocChunkStandardRegulationUI = createToolUI({
|
||||
toolName: "retrieve_doc_chunk_standard_regulation",
|
||||
iconSrc: `${process.env.NEXT_PUBLIC_API_URL_PREFIX || ''}/search.png`,
|
||||
titleKey: "toolDocumentSearch"
|
||||
});
|
||||
|
||||
export const RetrieveSystemUsermanualUI = createToolUI({
|
||||
toolName: "retrieve_system_usermanual",
|
||||
iconSrc: `${process.env.NEXT_PUBLIC_API_URL_PREFIX || ''}/user-guide.png`,
|
||||
titleKey: "toolSystemUserManual"
|
||||
});
|
||||
28
vw-agentic-rag/web/src/components/ui/AiAssistantMessage.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { AssistantActionBar, AssistantMessage, BranchPicker } from "@assistant-ui/react-ui";
|
||||
import { ActionBarPrimitive, useMessage } from "@assistant-ui/react";
|
||||
import TypingIndicator from "./typing-indicator";
|
||||
import { MarkdownText } from "./markdown-text";
|
||||
|
||||
const AiAssistantMessage = () => {
|
||||
const content = useMessage((m) => m.content)
|
||||
const status = useMessage((m) => m.status)
|
||||
const id = useMessage((m) => m.id)
|
||||
const isLast = useMessage((m) => m.isLast)
|
||||
const isEmpty = !content || content.length === 0
|
||||
const isRunning = status?.type === 'running'
|
||||
|
||||
return (
|
||||
<AssistantMessage.Root>
|
||||
{ isRunning && (
|
||||
<div className="col-start-3 row-start-2">
|
||||
<TypingIndicator />
|
||||
</div>
|
||||
) }
|
||||
<AssistantMessage.Content components={{ Text: MarkdownText }} />
|
||||
</AssistantMessage.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export default AiAssistantMessage;
|
||||
|
||||
|
||||
38
vw-agentic-rag/web/src/components/ui/markdown-text.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { MarkdownTextPrimitive, useIsMarkdownCodeBlock } from "@assistant-ui/react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import rehypeExternalLinks from "rehype-external-links";
|
||||
|
||||
|
||||
import { memo } from "react";
|
||||
|
||||
import rehypeRaw from "rehype-raw";
|
||||
|
||||
|
||||
const MarkdownTextImpl = () => {
|
||||
return (
|
||||
<MarkdownTextPrimitive
|
||||
remarkPlugins={[remarkGfm]}
|
||||
|
||||
rehypePlugins={[rehypeRaw,[rehypeExternalLinks, {
|
||||
target: "_blank",
|
||||
rel: ["noopener", "noreferrer"],
|
||||
}]]}
|
||||
components= {{
|
||||
sup: ({ node: _node, className, ...props }) => (
|
||||
<sup className= "!text-xs !relative !-top-1 [&>a]:!text-xs [&>a]:!no-underline" style={{
|
||||
verticalAlign: 'super',
|
||||
fontSize: '0.75em',
|
||||
position: 'relative',
|
||||
top: '-0.25rem'
|
||||
} as React.CSSProperties}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
className="prose prose-gray max-w-none [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 prose-a:text-blue-600 hover:prose-a:text-blue-800 prose-a:underline"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const MarkdownText = memo(MarkdownTextImpl);
|
||||
|
||||
74
vw-agentic-rag/web/src/components/ui/mychat.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Thread, ThreadWelcome, Composer, UserMessage } from "@assistant-ui/react-ui";
|
||||
import { MarkdownText } from "./markdown-text";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import AiAssistantMessage from "./AiAssistantMessage";
|
||||
import { MessagePrimitive, useComposerRuntime } from "@assistant-ui/react";
|
||||
import { FC } from "react";
|
||||
|
||||
// 自定义建议组件
|
||||
const ExampleQuestionButton: FC<{ question: string }> = ({ question }) => {
|
||||
const composer = useComposerRuntime();
|
||||
|
||||
const handleClick = () => {
|
||||
composer.setText(question);
|
||||
composer.send();
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className="aui-thread-welcome-suggestion"
|
||||
onClick={handleClick}
|
||||
>
|
||||
{question}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export const MyChat = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Thread.Root
|
||||
config={{
|
||||
welcome: { message: t.welcomeMessage },
|
||||
}}
|
||||
>
|
||||
|
||||
<Thread.Viewport >
|
||||
<ThreadWelcome.Root>
|
||||
<ThreadWelcome.Center>
|
||||
<ThreadWelcome.Avatar />
|
||||
<ThreadWelcome.Message />
|
||||
</ThreadWelcome.Center>
|
||||
|
||||
{/* 自定义示例问题 */}
|
||||
<div className="aui-thread-welcome-suggestions">
|
||||
{t.exampleQuestions.map((question, index) => (
|
||||
<ExampleQuestionButton
|
||||
key={index}
|
||||
question={question}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ThreadWelcome.Root>
|
||||
<Thread.Messages components={{ UserMessage: AiUserMessage, AssistantMessage: AiAssistantMessage }} />
|
||||
<Thread.ViewportFooter >
|
||||
<Thread.ScrollToBottom />
|
||||
<Composer />
|
||||
<p className="mt-2 text-xs text-muted-foreground"> {t.tooltip} </p>
|
||||
</Thread.ViewportFooter>
|
||||
</Thread.Viewport>
|
||||
|
||||
</Thread.Root>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export const AiUserMessage: FC = () => {
|
||||
return (
|
||||
<MessagePrimitive.Root className="aui-user-message-root">
|
||||
<div className="aui-user-message-content">
|
||||
<MessagePrimitive.Content />
|
||||
</div>
|
||||
</MessagePrimitive.Root>
|
||||
);
|
||||
};
|
||||
39
vw-agentic-rag/web/src/components/ui/typing-indicator.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
const TypingIndicator = () => {
|
||||
return (
|
||||
<div className="w-fit h-8 items-center rounded-md text-center px-2 flex gap-0.5 text-2xl">
|
||||
<div
|
||||
className="animate-bounce"
|
||||
style={{
|
||||
animationDelay: '0ms',
|
||||
animationDuration: '750ms',
|
||||
}}
|
||||
>
|
||||
.
|
||||
</div>
|
||||
<div
|
||||
className="animate-bounce"
|
||||
style={{
|
||||
animationDelay: '125ms',
|
||||
animationDuration: '750ms',
|
||||
}}
|
||||
>
|
||||
.
|
||||
</div>
|
||||
<div
|
||||
className="animate-bounce"
|
||||
style={{
|
||||
animationDelay: '250ms',
|
||||
animationDuration: '750ms',
|
||||
}}
|
||||
>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TypingIndicator
|
||||
36
vw-agentic-rag/web/src/hooks/useAppConfig.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface AppConfig {
|
||||
apiUrlPrefix: string;
|
||||
apiUrl: string;
|
||||
}
|
||||
|
||||
const defaultConfig: AppConfig = {
|
||||
apiUrlPrefix: '',
|
||||
apiUrl: 'http://localhost:8000',
|
||||
};
|
||||
|
||||
export function useAppConfig() {
|
||||
const [config, setConfig] = useState<AppConfig>(defaultConfig);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/config');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setConfig(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch config, using defaults:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchConfig();
|
||||
}, []);
|
||||
|
||||
return { config, loading };
|
||||
}
|
||||
35
vw-agentic-rag/web/src/hooks/useErrorHandler.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
export interface ErrorToastData {
|
||||
id: string;
|
||||
message: string;
|
||||
type: 'error' | 'warning' | 'network';
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing error toasts
|
||||
*/
|
||||
export const useErrorHandler = () => {
|
||||
const [errors, setErrors] = useState<ErrorToastData[]>([]);
|
||||
|
||||
const addError = useCallback((message: string, type: 'error' | 'warning' | 'network' = 'error') => {
|
||||
const id = Date.now().toString();
|
||||
setErrors(prev => [...prev, { id, message, type, timestamp: new Date() }]);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
setErrors(prev => prev.filter(error => error.id !== id));
|
||||
}, 5000);
|
||||
}, []);
|
||||
|
||||
const removeError = useCallback((id: string) => {
|
||||
setErrors(prev => prev.filter(error => error.id !== id));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
errors,
|
||||
addError,
|
||||
removeError
|
||||
};
|
||||
};
|
||||
28
vw-agentic-rag/web/src/hooks/useSessionId.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
const SESSION_STORAGE_KEY = 'chat_session_id';
|
||||
|
||||
export function useSessionId(): string {
|
||||
const [sessionId, setSessionId] = useState<string>(() => {
|
||||
// Always generate a new session ID on page load/refresh
|
||||
const newSessionId = `session_${Date.now()}_${Math.random().toString(36).substring(2)}`;
|
||||
return newSessionId;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Save to sessionStorage (not localStorage) for current browser tab only
|
||||
// sessionStorage gets cleared when tab is closed, providing session isolation
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionStorage.setItem(SESSION_STORAGE_KEY, sessionId);
|
||||
}
|
||||
}, [sessionId]);
|
||||
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
export function clearSessionId(): void {
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionStorage.removeItem(SESSION_STORAGE_KEY);
|
||||
localStorage.removeItem(SESSION_STORAGE_KEY); // Clean up old localStorage entries
|
||||
}
|
||||
}
|
||||
54
vw-agentic-rag/web/src/hooks/useTranslation.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Language,
|
||||
Translations,
|
||||
translations,
|
||||
getCurrentLanguage,
|
||||
switchLanguage as switchLang
|
||||
} from '../utils/i18n';
|
||||
|
||||
export function useTranslation() {
|
||||
const [language, setLanguage] = useState<Language>('zh');
|
||||
const [t, setT] = useState<Translations>(translations.zh);
|
||||
|
||||
useEffect(() => {
|
||||
// 初始化语言设置
|
||||
const currentLang = getCurrentLanguage();
|
||||
setLanguage(currentLang);
|
||||
setT(translations[currentLang]);
|
||||
|
||||
// 监听语言变化事件
|
||||
const handleLanguageChange = () => {
|
||||
const newLang = getCurrentLanguage();
|
||||
setLanguage(newLang);
|
||||
setT(translations[newLang]);
|
||||
};
|
||||
|
||||
window.addEventListener('languagechange', handleLanguageChange);
|
||||
|
||||
// 监听URL变化
|
||||
const handlePopstate = () => {
|
||||
handleLanguageChange();
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', handlePopstate);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('languagechange', handleLanguageChange);
|
||||
window.removeEventListener('popstate', handlePopstate);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const switchLanguage = (newLanguage: Language) => {
|
||||
switchLang(newLanguage);
|
||||
setLanguage(newLanguage);
|
||||
setT(translations[newLanguage]);
|
||||
};
|
||||
|
||||
return {
|
||||
language,
|
||||
t,
|
||||
switchLanguage,
|
||||
availableLanguages: ['zh', 'en'] as Language[],
|
||||
};
|
||||
}
|
||||
39
vw-agentic-rag/web/src/utils/chatApi.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Client } from "@langchain/langgraph-sdk";
|
||||
import { LangChainMessage } from "@assistant-ui/react-langgraph";
|
||||
|
||||
const createClient = () => {
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
|
||||
|
||||
return new Client({
|
||||
apiUrl,
|
||||
});
|
||||
};
|
||||
|
||||
export const createThread = async () => {
|
||||
const client = createClient();
|
||||
return client.threads.create();
|
||||
};
|
||||
|
||||
export const getThreadState = async (
|
||||
threadId: string,
|
||||
) => {
|
||||
const client = createClient();
|
||||
return client.threads.getState(threadId);
|
||||
};
|
||||
|
||||
export const sendMessage = async (params: {
|
||||
threadId: string;
|
||||
messages: LangChainMessage;
|
||||
}) => {
|
||||
const client = createClient();
|
||||
return client.runs.stream(
|
||||
params.threadId,
|
||||
process.env["NEXT_PUBLIC_LANGGRAPH_ASSISTANT_ID"]!,
|
||||
{
|
||||
input: {
|
||||
messages: params.messages,
|
||||
},
|
||||
streamMode: "messages",
|
||||
},
|
||||
);
|
||||
};
|
||||
222
vw-agentic-rag/web/src/utils/i18n.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
// 多语言支持配置
|
||||
export type Language = 'zh' | 'en';
|
||||
|
||||
export interface Translations {
|
||||
// 通用
|
||||
loading: string;
|
||||
error: string;
|
||||
expand: string;
|
||||
collapse: string;
|
||||
|
||||
// 页面标题和描述
|
||||
appTitle: string;
|
||||
appDescription: string;
|
||||
welcomeMessage: string;
|
||||
|
||||
// 工具相关
|
||||
toolSearching: string;
|
||||
toolProcessing: string;
|
||||
toolCompleted: string;
|
||||
toolFailed: string;
|
||||
toolStandardSearch: string;
|
||||
toolDocumentSearch: string;
|
||||
toolSystemUserManual: string;
|
||||
toolQuery: string;
|
||||
toolFound: string;
|
||||
toolRetrieved: string;
|
||||
toolResults: string;
|
||||
toolChunks: string;
|
||||
toolMoreResults: string;
|
||||
toolMoreChunks: string;
|
||||
toolDocumentChunk: string;
|
||||
|
||||
// 状态消息
|
||||
statusSearching: string;
|
||||
statusProcessing: string;
|
||||
statusCompleted: string;
|
||||
statusError: string;
|
||||
|
||||
// 新增:工具汇总相关
|
||||
toolExecutionSummary: string;
|
||||
resultsText: string;
|
||||
tooltip: string;
|
||||
|
||||
// 示例问题
|
||||
exampleQuestions: string[];
|
||||
}
|
||||
|
||||
export const translations: Record<Language, Translations> = {
|
||||
zh: {
|
||||
// 通用
|
||||
loading: '加载中...',
|
||||
error: '错误',
|
||||
expand: '展开',
|
||||
collapse: '收起',
|
||||
|
||||
// 页面标题和描述
|
||||
appTitle: 'CATOnline AI助手',
|
||||
appDescription: '',
|
||||
welcomeMessage: `你好,我是 CATOnline AI 助手。
|
||||
我能基于智能检索,解答中国标准与法律法规,并提供本系统的使用指引。
|
||||
海外标准与法规功能即将上线,敬请期待。
|
||||
您可以尝试如下示例提问:`,
|
||||
|
||||
// 工具相关
|
||||
toolSearching: '搜索中...',
|
||||
toolProcessing: '处理中...',
|
||||
toolCompleted: '已完成',
|
||||
toolFailed: '失败',
|
||||
toolStandardSearch: '标准/法规语义检索',
|
||||
toolDocumentSearch: '文档内容语义检索',
|
||||
toolSystemUserManual: '系统用户手册内容检索',
|
||||
toolQuery: '查询',
|
||||
toolFound: '找到',
|
||||
toolRetrieved: '获取',
|
||||
toolResults: '条结果',
|
||||
toolChunks: '片段',
|
||||
toolMoreResults: '还有',
|
||||
toolMoreChunks: '还有',
|
||||
toolDocumentChunk: '文档片段',
|
||||
|
||||
// 状态消息
|
||||
statusSearching: '搜索中...',
|
||||
statusProcessing: '处理中...',
|
||||
statusCompleted: '已完成',
|
||||
statusError: '发生错误',
|
||||
|
||||
// 工具汇总相关
|
||||
toolExecutionSummary: '工具执行汇总',
|
||||
resultsText: '个结果',
|
||||
tooltip: 'AI 可能会出错,请核对重要信息。',
|
||||
|
||||
// 示例问题
|
||||
exampleQuestions: [
|
||||
'自动驾驶L2和L3的定义',
|
||||
'电力储能用锂离子电池最新标准发布时间?',
|
||||
'根据标准,如何测试电动汽车充电功能的兼容性',
|
||||
'介绍CATOnline系统',
|
||||
]
|
||||
},
|
||||
|
||||
en: {
|
||||
// 通用
|
||||
loading: 'Loading...',
|
||||
error: 'Error',
|
||||
expand: 'Expand',
|
||||
collapse: 'Collapse',
|
||||
|
||||
// 页面标题和描述
|
||||
appTitle: 'CATOnline AI Assistant',
|
||||
appDescription: '',
|
||||
welcomeMessage: `Hello, I’m the CATOnline AI Assistant.
|
||||
I can answer questions about Chinese standards and laws/regulations based on intelligent search, and provide guidance on using this system.
|
||||
Support for overseas standards and regulations will be launched soon—please stay tuned.
|
||||
You can try the following sample questions:`,
|
||||
|
||||
// 工具相关
|
||||
toolSearching: 'Searching...',
|
||||
toolProcessing: 'Processing...',
|
||||
toolCompleted: 'Completed',
|
||||
toolFailed: 'Failed',
|
||||
toolStandardSearch: 'Standard/Regulation Semantic Retrieval',
|
||||
toolDocumentSearch: 'Document Content Semantic Retrieval',
|
||||
toolSystemUserManual: 'System User Manual Content Retrieval',
|
||||
toolQuery: 'Query',
|
||||
toolFound: 'Found',
|
||||
toolRetrieved: 'Retrieved',
|
||||
toolResults: 'results',
|
||||
toolChunks: 'Chunks',
|
||||
toolMoreResults: 'more results',
|
||||
toolMoreChunks: 'more chunks',
|
||||
toolDocumentChunk: 'Document Chunks',
|
||||
|
||||
// 状态消息
|
||||
statusSearching: 'Searching...',
|
||||
statusProcessing: 'Processing...',
|
||||
statusCompleted: 'Completed',
|
||||
statusError: 'Error occurred',
|
||||
|
||||
// 工具汇总相关
|
||||
toolExecutionSummary: 'Tool Execution Summary',
|
||||
resultsText: 'results',
|
||||
tooltip: 'AI can make mistakes. Please check important info',
|
||||
|
||||
// 示例问题
|
||||
exampleQuestions: [
|
||||
'Definition of L2 and L3 in autonomous driving',
|
||||
'When was the latest standard for lithium-ion batteries for power storage released?',
|
||||
'According to the standard, how to test the compatibility of electric vehicle charging function?',
|
||||
'Introduce CATOnline system',
|
||||
]
|
||||
},
|
||||
};
|
||||
|
||||
// 获取浏览器首选语言
|
||||
export function getBrowserLanguage(): Language {
|
||||
if (typeof window === 'undefined') return 'zh'; // SSR fallback
|
||||
|
||||
const browserLang = navigator.language.toLowerCase();
|
||||
|
||||
// 检查是否为中文
|
||||
if (browserLang.startsWith('zh')) {
|
||||
return 'zh';
|
||||
}
|
||||
|
||||
// 默认返回英文
|
||||
return 'en';
|
||||
}
|
||||
|
||||
// 从URL参数获取语言设置
|
||||
export function getLanguageFromURL(): Language | null {
|
||||
if (typeof window === 'undefined') return null; // SSR fallback
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const langParam = urlParams.get('lang')?.toLowerCase();
|
||||
|
||||
if (langParam === 'zh' || langParam === 'en') {
|
||||
return langParam as Language;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取当前应该使用的语言
|
||||
export function getCurrentLanguage(): Language {
|
||||
// 优先使用URL参数
|
||||
const urlLang = getLanguageFromURL();
|
||||
if (urlLang) {
|
||||
return urlLang;
|
||||
}
|
||||
|
||||
// 其次使用localStorage保存的语言
|
||||
if (typeof window !== 'undefined') {
|
||||
const savedLang = localStorage.getItem('preferred-language') as Language;
|
||||
if (savedLang && (savedLang === 'zh' || savedLang === 'en')) {
|
||||
return savedLang;
|
||||
}
|
||||
}
|
||||
|
||||
// 最后使用浏览器语言
|
||||
return getBrowserLanguage();
|
||||
}
|
||||
|
||||
// 保存语言设置到localStorage
|
||||
export function saveLanguagePreference(language: Language) {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('preferred-language', language);
|
||||
}
|
||||
}
|
||||
|
||||
// 切换语言并更新URL
|
||||
export function switchLanguage(language: Language) {
|
||||
saveLanguagePreference(language);
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('lang', language);
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
|
||||
// 触发重新渲染
|
||||
window.dispatchEvent(new Event('languagechange'));
|
||||
}
|
||||
}
|
||||
194
vw-agentic-rag/web/src/utils/i18n.ts-demo
Normal file
@@ -0,0 +1,194 @@
|
||||
// 多语言支持配置
|
||||
export type Language = 'zh' | 'en';
|
||||
|
||||
export interface Translations {
|
||||
// 通用
|
||||
loading: string;
|
||||
error: string;
|
||||
expand: string;
|
||||
collapse: string;
|
||||
|
||||
// 页面标题和描述
|
||||
appTitle: string;
|
||||
appDescription: string;
|
||||
welcomeMessage: string;
|
||||
|
||||
// 工具相关
|
||||
toolSearching: string;
|
||||
toolProcessing: string;
|
||||
toolCompleted: string;
|
||||
toolFailed: string;
|
||||
toolStandardSearch: string;
|
||||
toolDocumentSearch: string;
|
||||
toolQuery: string;
|
||||
toolFound: string;
|
||||
toolRetrieved: string;
|
||||
toolResults: string;
|
||||
toolChunks: string;
|
||||
toolMoreResults: string;
|
||||
toolMoreChunks: string;
|
||||
toolDocumentChunk: string;
|
||||
|
||||
// 状态消息
|
||||
statusSearching: string;
|
||||
statusProcessing: string;
|
||||
statusCompleted: string;
|
||||
statusError: string;
|
||||
|
||||
// 新增:工具汇总相关
|
||||
toolExecutionSummary: string;
|
||||
resultsText: string;
|
||||
tooltip: string;
|
||||
}
|
||||
|
||||
export const translations: Record<Language, Translations> = {
|
||||
zh: {
|
||||
// 通用
|
||||
loading: '加载中...',
|
||||
error: '错误',
|
||||
expand: '展开',
|
||||
collapse: '收起',
|
||||
|
||||
// 页面标题和描述
|
||||
appTitle: '代理式RAG',
|
||||
appDescription: '先进的AI代理,支持RAG检索和工具调用',
|
||||
welcomeMessage: '你好!我是你的 AI 助手。我可以解答有关标准和法规的问题,基于从知识库智能检索到的信息。',
|
||||
|
||||
// 工具相关
|
||||
toolSearching: '搜索中...',
|
||||
toolProcessing: '处理中...',
|
||||
toolCompleted: '已完成',
|
||||
toolFailed: '失败',
|
||||
toolStandardSearch: '标准/法规语义检索',
|
||||
toolDocumentSearch: '文档块语义检索',
|
||||
toolQuery: '查询',
|
||||
toolFound: '找到',
|
||||
toolRetrieved: '获取',
|
||||
toolResults: '条结果',
|
||||
toolChunks: '片段',
|
||||
toolMoreResults: '还有',
|
||||
toolMoreChunks: '还有',
|
||||
toolDocumentChunk: '文档块',
|
||||
|
||||
// 状态消息
|
||||
statusSearching: '搜索中...',
|
||||
statusProcessing: '处理中...',
|
||||
statusCompleted: '已完成',
|
||||
statusError: '发生错误',
|
||||
|
||||
// 工具汇总相关
|
||||
toolExecutionSummary: '工具执行汇总',
|
||||
resultsText: '个结果',
|
||||
tooltip: 'AI 可能会出错,请核对重要信息。'
|
||||
},
|
||||
|
||||
en: {
|
||||
// 通用
|
||||
loading: 'Loading...',
|
||||
error: 'Error',
|
||||
expand: 'Expand',
|
||||
collapse: 'Collapse',
|
||||
|
||||
// 页面标题和描述
|
||||
appTitle: 'Agentic RAG',
|
||||
appDescription: 'Advanced AI Agent with RAG and Tool Support',
|
||||
welcomeMessage: 'Hello! I\'m AI agent that answer your questions about standards and regulations, grounded on information intelligently retrieved from the knowledge base.',
|
||||
|
||||
// 工具相关
|
||||
toolSearching: 'Searching...',
|
||||
toolProcessing: 'Processing...',
|
||||
toolCompleted: 'Completed',
|
||||
toolFailed: 'Failed',
|
||||
toolStandardSearch: 'Standard/Regulation Semantic Retrieval',
|
||||
toolDocumentSearch: 'Document Chunk Semantic Retrieval',
|
||||
toolQuery: 'Query',
|
||||
toolFound: 'Found',
|
||||
toolRetrieved: 'Retrieved',
|
||||
toolResults: 'results',
|
||||
toolChunks: 'Fragment',
|
||||
toolMoreResults: 'more results',
|
||||
toolMoreChunks: 'more chunks',
|
||||
toolDocumentChunk: 'Document Chunk',
|
||||
|
||||
// 状态消息
|
||||
statusSearching: 'Searching...',
|
||||
statusProcessing: 'Processing...',
|
||||
statusCompleted: 'Completed',
|
||||
statusError: 'Error occurred',
|
||||
|
||||
// 工具汇总相关
|
||||
toolExecutionSummary: 'Tool Execution Summary',
|
||||
resultsText: 'results',
|
||||
tooltip: 'AI can make mistakes. Please check important info'
|
||||
},
|
||||
};
|
||||
|
||||
// 获取浏览器首选语言
|
||||
export function getBrowserLanguage(): Language {
|
||||
if (typeof window === 'undefined') return 'zh'; // SSR fallback
|
||||
|
||||
const browserLang = navigator.language.toLowerCase();
|
||||
|
||||
// 检查是否为中文
|
||||
if (browserLang.startsWith('zh')) {
|
||||
return 'zh';
|
||||
}
|
||||
|
||||
// 默认返回英文
|
||||
return 'en';
|
||||
}
|
||||
|
||||
// 从URL参数获取语言设置
|
||||
export function getLanguageFromURL(): Language | null {
|
||||
if (typeof window === 'undefined') return null; // SSR fallback
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const langParam = urlParams.get('lang')?.toLowerCase();
|
||||
|
||||
if (langParam === 'zh' || langParam === 'en') {
|
||||
return langParam as Language;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取当前应该使用的语言
|
||||
export function getCurrentLanguage(): Language {
|
||||
// 优先使用URL参数
|
||||
const urlLang = getLanguageFromURL();
|
||||
if (urlLang) {
|
||||
return urlLang;
|
||||
}
|
||||
|
||||
// 其次使用localStorage保存的语言
|
||||
if (typeof window !== 'undefined') {
|
||||
const savedLang = localStorage.getItem('preferred-language') as Language;
|
||||
if (savedLang && (savedLang === 'zh' || savedLang === 'en')) {
|
||||
return savedLang;
|
||||
}
|
||||
}
|
||||
|
||||
// 最后使用浏览器语言
|
||||
return getBrowserLanguage();
|
||||
}
|
||||
|
||||
// 保存语言设置到localStorage
|
||||
export function saveLanguagePreference(language: Language) {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('preferred-language', language);
|
||||
}
|
||||
}
|
||||
|
||||
// 切换语言并更新URL
|
||||
export function switchLanguage(language: Language) {
|
||||
saveLanguagePreference(language);
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('lang', language);
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
|
||||
// 触发重新渲染
|
||||
window.dispatchEvent(new Event('languagechange'));
|
||||
}
|
||||
}
|
||||
56
vw-agentic-rag/web/tailwind.config.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
"./src/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require("tailwindcss-animate"),
|
||||
require("@tailwindcss/typography"),
|
||||
require("@assistant-ui/react-ui/tailwindcss")({
|
||||
components: ["thread", "thread-list"],
|
||||
shadcn: true
|
||||
})
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
27
vw-agentic-rag/web/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||