This commit is contained in:
2025-09-26 17:15:54 +08:00
commit db0e5965ec
211 changed files with 40437 additions and 0 deletions

17
vw-agentic-rag/web/.env Normal file
View 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

View 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
View 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

View 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.

View 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;

View 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;

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
export default config;

View 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

View 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
View 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;
}
})();

View 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>

View 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>

View 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>

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View 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

View 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

View 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',
},
});
}

View 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",
});
}

View 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'
});
}

View 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(),
},
});
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View 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;
}
}

View 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>
);
}

View File

@@ -0,0 +1,7 @@
"use client";
import { Assistant } from "../components/Assistant";
export default function ChatPage() {
return (<Assistant></Assistant> );
}

View 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>
);
}

View 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;
}
}

View 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>
);
};

View 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>
);
}

View 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"
});

View 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;

View 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);

View 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>
);
};

View 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

View 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 };
}

View 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
};
};

View 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
}
}

View 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[],
};
}

View 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",
},
);
};

View 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, Im 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'));
}
}

View 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'));
}
}

View 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;

View 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"]
}