mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-15 20:16:02 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60f9320c31 | |||
| 01de2d07ee | |||
| 09b5be56b2 | |||
| ec3640da1c | |||
| e9b09d7189 | |||
| 88838f6538 | |||
| b69c6649a3 | |||
| 133221823e | |||
| 9906ddc416 | |||
| 40f4ad735b | |||
| 8ed5326094 | |||
| 3a8a464610 | |||
| c19974fae8 | |||
| 74679f984e | |||
| f32dbaa4f0 |
@@ -260,6 +260,13 @@ OPENAI_API_KEY=sk-xxxxxxxxx
|
||||
# Authentication across different domains , use,to splite different origin
|
||||
# NEXT_PUBLIC_CLERK_AUTH_ALLOW_ORIGINS='https://market.lobehub.com,https://lobehub.com'
|
||||
|
||||
|
||||
# Comma separated redirect URIs for the marketplace OIDC client
|
||||
# MARKET_OIDC_REDIRECT_URIS=https://market.lobehub.com/oidc/callback,http://localhost:8787/market-oidc/consent/callback
|
||||
|
||||
# Comma separated post logout redirect URIs for the marketplace OIDC client
|
||||
# MARKET_OIDC_POST_LOGOUT_URIS=https://market.lobehub.com/oidc/logout,http://localhost:8787/market-oidc/logout
|
||||
|
||||
# NextAuth related configurations
|
||||
# NEXT_PUBLIC_ENABLE_NEXT_AUTH=1
|
||||
# NEXT_AUTH_SECRET=
|
||||
|
||||
@@ -358,6 +358,9 @@ export default class Browser {
|
||||
session: browserWindow.webContents.session,
|
||||
});
|
||||
|
||||
// Setup CORS bypass for local file server
|
||||
this.setupCORSBypass(browserWindow);
|
||||
|
||||
logger.debug(`[${this.identifier}] Initiating placeholder and URL loading sequence.`);
|
||||
this.loadPlaceholder().then(() => {
|
||||
this.loadUrl(path).catch((e) => {
|
||||
@@ -491,4 +494,37 @@ export default class Browser {
|
||||
logger.debug(`[${this.identifier}] Manually reapplying visual effects via Browser.`);
|
||||
this.applyVisualEffects();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup CORS bypass for local file server (127.0.0.1:*)
|
||||
* This is needed for Electron to access files from the local static file server
|
||||
*/
|
||||
private setupCORSBypass(browserWindow: BrowserWindow): void {
|
||||
logger.debug(`[${this.identifier}] Setting up CORS bypass for local file server`);
|
||||
|
||||
const session = browserWindow.webContents.session;
|
||||
|
||||
// Intercept response headers to add CORS headers
|
||||
session.webRequest.onHeadersReceived((details, callback) => {
|
||||
const url = details.url;
|
||||
|
||||
// Only modify headers for local file server requests (127.0.0.1)
|
||||
if (url.includes('127.0.0.1') || url.includes('lobe-desktop-file')) {
|
||||
const responseHeaders = details.responseHeaders || {};
|
||||
|
||||
// Add CORS headers
|
||||
responseHeaders['Access-Control-Allow-Origin'] = ['*'];
|
||||
responseHeaders['Access-Control-Allow-Methods'] = ['GET, POST, PUT, DELETE, OPTIONS'];
|
||||
responseHeaders['Access-Control-Allow-Headers'] = ['*'];
|
||||
|
||||
callback({
|
||||
responseHeaders,
|
||||
});
|
||||
} else {
|
||||
callback({ responseHeaders: details.responseHeaders });
|
||||
}
|
||||
});
|
||||
|
||||
logger.debug(`[${this.identifier}] CORS bypass setup completed`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,21 @@ import type { App } from '../App';
|
||||
|
||||
const logger = createLogger('core:StaticFileServerManager');
|
||||
|
||||
const getAllowedOrigin = (rawOrigin?: string) => {
|
||||
if (!rawOrigin) return '*';
|
||||
|
||||
try {
|
||||
const url = new URL(rawOrigin);
|
||||
const normalizedOrigin = `${url.protocol}//${url.host}`;
|
||||
return url.hostname === 'localhost' || url.hostname === '127.0.0.1' ? normalizedOrigin : '*';
|
||||
} catch {
|
||||
const normalizedOrigin = rawOrigin.replace(/\/$/, '');
|
||||
return normalizedOrigin.includes('localhost') || normalizedOrigin.includes('127.0.0.1')
|
||||
? normalizedOrigin
|
||||
: '*';
|
||||
}
|
||||
};
|
||||
|
||||
export class StaticFileServerManager {
|
||||
private app: App;
|
||||
private fileService: FileService;
|
||||
@@ -126,16 +141,38 @@ export class StaticFileServerManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取请求的 Origin 并设置 CORS
|
||||
const origin = req.headers.origin || req.headers.referer;
|
||||
const allowedOrigin = getAllowedOrigin(origin);
|
||||
|
||||
// 处理 CORS 预检请求
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(204, {
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||
'Access-Control-Allow-Origin': allowedOrigin,
|
||||
'Access-Control-Max-Age': '86400',
|
||||
});
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(req.url, `http://127.0.0.1:${this.serverPort}`);
|
||||
logger.debug(`Processing HTTP file request: ${req.url}`);
|
||||
logger.debug(`Request method: ${req.method}`);
|
||||
logger.debug(`Request headers: ${JSON.stringify(req.headers)}`);
|
||||
|
||||
// 提取文件路径:从 /desktop-file/path/to/file.png 中提取相对路径
|
||||
let filePath = decodeURIComponent(url.pathname.slice(1)); // 移除开头的 /
|
||||
logger.debug(`Initial file path after decode: ${filePath}`);
|
||||
|
||||
// 如果路径以 desktop-file/ 开头,则移除该前缀
|
||||
const prefixWithoutSlash = LOCAL_STORAGE_URL_PREFIX.slice(1) + '/'; // 移除开头的 / 并添加结尾的 /
|
||||
logger.debug(`Prefix to remove: ${prefixWithoutSlash}`);
|
||||
|
||||
if (filePath.startsWith(prefixWithoutSlash)) {
|
||||
filePath = filePath.slice(prefixWithoutSlash.length);
|
||||
logger.debug(`File path after removing prefix: ${filePath}`);
|
||||
}
|
||||
|
||||
if (!filePath) {
|
||||
@@ -148,7 +185,12 @@ export class StaticFileServerManager {
|
||||
}
|
||||
|
||||
// 使用 FileService 获取文件
|
||||
const fileResult = await this.fileService.getFile(`desktop://${filePath}`);
|
||||
const desktopPath = `desktop://${filePath}`;
|
||||
logger.debug(`Attempting to get file: ${desktopPath}`);
|
||||
const fileResult = await this.fileService.getFile(desktopPath);
|
||||
logger.debug(
|
||||
`File retrieved successfully, mime type: ${fileResult.mimeType}, size: ${fileResult.content.byteLength} bytes`,
|
||||
);
|
||||
|
||||
// 再次检查响应状态
|
||||
if (res.destroyed || res.headersSent) {
|
||||
@@ -158,11 +200,8 @@ export class StaticFileServerManager {
|
||||
|
||||
// 设置响应头
|
||||
res.writeHead(200, {
|
||||
// 缓存一年
|
||||
'Access-Control-Allow-Origin': 'http://localhost:*',
|
||||
|
||||
'Access-Control-Allow-Origin': allowedOrigin,
|
||||
'Cache-Control': 'public, max-age=31536000',
|
||||
// 允许 localhost 的任意端口
|
||||
'Content-Length': Buffer.byteLength(fileResult.content),
|
||||
'Content-Type': fileResult.mimeType,
|
||||
});
|
||||
@@ -173,16 +212,27 @@ export class StaticFileServerManager {
|
||||
logger.debug(`HTTP file served successfully: desktop://${filePath}`);
|
||||
} catch (error) {
|
||||
logger.error(`Error serving HTTP file: ${error}`);
|
||||
logger.error(`Error stack: ${error.stack}`);
|
||||
|
||||
// 检查响应是否仍然可写
|
||||
if (!res.destroyed && !res.headersSent) {
|
||||
try {
|
||||
// 获取请求的 Origin 并设置 CORS(错误响应也需要!)
|
||||
const origin = req.headers.origin || req.headers.referer;
|
||||
const allowedOrigin = getAllowedOrigin(origin);
|
||||
|
||||
// 判断是否是文件未找到错误
|
||||
if (error.name === 'FileNotFoundError') {
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
res.writeHead(404, {
|
||||
'Access-Control-Allow-Origin': allowedOrigin,
|
||||
'Content-Type': 'text/plain',
|
||||
});
|
||||
res.end('File Not Found');
|
||||
} else {
|
||||
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
||||
res.writeHead(500, {
|
||||
'Access-Control-Allow-Origin': allowedOrigin,
|
||||
'Content-Type': 'text/plain',
|
||||
});
|
||||
res.end('Internal Server Error');
|
||||
}
|
||||
} catch (writeError) {
|
||||
|
||||
@@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V مبني على نموذج GLM-4.5-Air الأساسي، يرث التقنيات المثبتة من GLM-4.1V-Thinking، ويوسعها بفعالية من خلال بنية MoE القوية التي تضم 106 مليار معلمة."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V е изграден върху основния модел GLM-4.5-Air, наследявайки проверените технологии на GLM-4.1V-Thinking и постига ефективно мащабиране чрез мощната MoE архитектура с 106 милиарда параметри."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V basiert auf dem GLM-4.5-Air Basismodell, übernimmt bewährte Techniken von GLM-4.1V-Thinking und skaliert effektiv mit einer leistungsstarken MoE-Architektur mit 106 Milliarden Parametern."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V is built on the GLM-4.5-Air foundational model, inheriting the proven techniques of GLM-4.1V-Thinking while achieving efficient scaling through a powerful 106 billion parameter MoE architecture."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V está construido sobre el modelo base GLM-4.5-Air, heredando la tecnología verificada de GLM-4.1V-Thinking y logrando una escalabilidad eficiente mediante una potente arquitectura MoE de 106 mil millones de parámetros."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V بر پایه مدل پایه GLM-4.5-Air ساخته شده است، فناوری اثبات شده GLM-4.1V-Thinking را به ارث برده و در عین حال با معماری قدرتمند MoE با 106 میلیارد پارامتر به طور مؤثر مقیاسپذیر شده است."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V est construit sur le modèle de base GLM-4.5-Air, héritant des techniques éprouvées de GLM-4.1V-Thinking, tout en réalisant une mise à l'échelle efficace grâce à une puissante architecture MoE de 106 milliards de paramètres."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V は GLM-4.5-Air 基盤モデルに基づき、GLM-4.1V-Thinking の検証済み技術を継承しつつ、強力な1060億パラメータの MoE アーキテクチャで効率的にスケールアップしています。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V는 GLM-4.5-Air 기본 모델을 기반으로 구축되었으며, 검증된 GLM-4.1V-Thinking 기술을 계승하면서 강력한 1060억 매개변수 MoE 아키텍처를 통해 효율적인 확장을 실현했습니다."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V is gebouwd op het GLM-4.5-Air basismodel, erft de bewezen technologie van GLM-4.1V-Thinking en realiseert efficiënte schaalvergroting via een krachtige MoE-architectuur met 106 miljard parameters."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V zbudowany jest na bazie GLM-4.5-Air, dziedzicząc zweryfikowane technologie GLM-4.1V-Thinking, jednocześnie skutecznie skalując się dzięki potężnej architekturze MoE z 106 miliardami parametrów."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V построена на базе GLM-4.5-Air, наследуя проверенные технологии GLM-4.1V-Thinking и обеспечивая эффективное масштабирование благодаря мощной архитектуре MoE с 106 миллиардами параметров."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V, GLM-4.5-Air temel modeli üzerine inşa edilmiştir, GLM-4.1V-Thinking'in doğrulanmış teknolojisini devralır ve güçlü 106 milyar parametreli MoE mimarisi ile etkili ölçeklenebilirlik sağlar."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V được xây dựng trên mô hình nền tảng GLM-4.5-Air, kế thừa công nghệ đã được xác minh của GLM-4.1V-Thinking, đồng thời mở rộng hiệu quả với kiến trúc MoE 106 tỷ tham số mạnh mẽ."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3314,4 +3314,4 @@
|
||||
"zai/glm-4.5v": {
|
||||
"description": "GLM-4.5V 基於 GLM-4.5-Air 基礎模型構建,繼承了 GLM-4.1V-Thinking 的經過驗證的技術,同時透過強大的 1060 億參數 MoE 架構實現了有效的擴展。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export interface WindowsDispatchEvents {
|
||||
* @param templateId Template identifier
|
||||
* @returns Operation result
|
||||
*/
|
||||
closeWindowsByTemplate: (templateId: string) => { error?: string, success: boolean; };
|
||||
closeWindowsByTemplate: (templateId: string) => { error?: string; success: boolean };
|
||||
|
||||
/**
|
||||
* Create a new multi-instance window
|
||||
|
||||
@@ -663,7 +663,8 @@ const aihubmixModels: AIChatModelCard[] = [
|
||||
vision: true,
|
||||
},
|
||||
contextWindowTokens: 200_000,
|
||||
description: 'Claude Haiku 4.5 是 Anthropic 最快且最智能的 Haiku 模型,具有闪电般的速度和扩展思考能力。',
|
||||
description:
|
||||
'Claude Haiku 4.5 是 Anthropic 最快且最智能的 Haiku 模型,具有闪电般的速度和扩展思考能力。',
|
||||
displayName: 'Claude Haiku 4.5',
|
||||
enabled: true,
|
||||
id: 'claude-haiku-4-5-20251001',
|
||||
|
||||
@@ -4,8 +4,8 @@ import { AIChatModelCard } from '../types/aiModel';
|
||||
const novitaChatModels: AIChatModelCard[] = [
|
||||
{
|
||||
abilities: {
|
||||
vision: true,
|
||||
functionCall: true,
|
||||
vision: true,
|
||||
},
|
||||
contextWindowTokens: 131_072,
|
||||
displayName: 'Qwen3 VL 235B A22B Instruct',
|
||||
@@ -21,8 +21,8 @@ const novitaChatModels: AIChatModelCard[] = [
|
||||
},
|
||||
{
|
||||
abilities: {
|
||||
vision: true,
|
||||
reasoning: true,
|
||||
vision: true,
|
||||
},
|
||||
contextWindowTokens: 131_072,
|
||||
displayName: 'Qwen3 VL 235B A22B Thinking',
|
||||
|
||||
@@ -50,7 +50,7 @@ const siliconcloudChatModels: AIChatModelCard[] = [
|
||||
abilities: {
|
||||
vision: true,
|
||||
},
|
||||
contextWindowTokens: 8_192,
|
||||
contextWindowTokens: 8192,
|
||||
description:
|
||||
'DeepSeek-OCR 是由深度求索(DeepSeek AI)推出的一个视觉语言模型,专注于光学字符识别(OCR)与"上下文光学压缩"。该模型旨在探索从图像中压缩上下文信息的边界,能够高效处理文档并将其转换为如 Markdown 等结构化文本格式。它能够准确识别图像中的文字内容,特别适用于文档数字化、文字提取和结构化处理等应用场景。',
|
||||
displayName: 'DeepSeek OCR',
|
||||
|
||||
@@ -17,11 +17,11 @@ export const LobeMinimaxAI = createOpenAICompatibleRuntime({
|
||||
|
||||
const minimaxTools = enabledSearch
|
||||
? [
|
||||
...(tools || []),
|
||||
{
|
||||
type: 'web_search',
|
||||
},
|
||||
]
|
||||
...(tools || []),
|
||||
{
|
||||
type: 'web_search',
|
||||
},
|
||||
]
|
||||
: tools;
|
||||
|
||||
// Resolve parameters with constraints
|
||||
|
||||
@@ -15,6 +15,7 @@ export const params = {
|
||||
models: async ({ client }) => {
|
||||
const modelsPage = (await client.models.list()) as any;
|
||||
const modelList = modelsPage.data.map((model: any) => {
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
|
||||
const { created, ...rest } = model;
|
||||
return rest;
|
||||
});
|
||||
|
||||
@@ -32,6 +32,9 @@ const TopActions = memo<TopActionProps>(({ tab, isPinned }) => {
|
||||
useServerConfigStore(featureFlagsSelectors);
|
||||
const hotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.NavigateToChat));
|
||||
|
||||
// Check if server mode is enabled
|
||||
const isServerMode = process.env.NEXT_PUBLIC_SERVICE_MODE === 'server';
|
||||
|
||||
const isChatActive = tab === SidebarTabKey.Chat && !isPinned;
|
||||
const isFilesActive = tab === SidebarTabKey.Files;
|
||||
const isDiscoverActive = tab === SidebarTabKey.Discover;
|
||||
@@ -66,8 +69,8 @@ const TopActions = memo<TopActionProps>(({ tab, isPinned }) => {
|
||||
tooltipProps={{ placement: 'right' }}
|
||||
/>
|
||||
</Link>
|
||||
{enableKnowledgeBase && (
|
||||
<Link aria-label={t('tab.knowledgeBase')} href={'/files'}>
|
||||
{enableKnowledgeBase && isServerMode && (
|
||||
<Link aria-label={t('tab.knowledgeBase')} href={'/knowledge'}>
|
||||
<ActionIcon
|
||||
active={isFilesActive}
|
||||
icon={FolderClosed}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { withSuspense } from '@/components/withSuspense';
|
||||
import { useDiscoverStore } from '@/store/discover';
|
||||
import { DiscoverTab } from '@/types/discover';
|
||||
|
||||
import NotFound from '../components/NotFound';
|
||||
import Breadcrumb from '../features/Breadcrumb';
|
||||
import { TocProvider } from '../features/Toc/useToc';
|
||||
import NotFound from '../components/NotFound';
|
||||
import { DetailProvider } from './[...slugs]/features/DetailProvider';
|
||||
import Details from './[...slugs]/features/Details';
|
||||
import Header from './[...slugs]/features/Header';
|
||||
|
||||
@@ -8,13 +8,7 @@ const NotFound = memo(() => {
|
||||
const { t } = useTranslation('error', { keyPrefix: 'notFound' });
|
||||
|
||||
return (
|
||||
<Flexbox
|
||||
align="center"
|
||||
height="100%"
|
||||
justify="center"
|
||||
style={{ minHeight: 400 }}
|
||||
width="100%"
|
||||
>
|
||||
<Flexbox align="center" height="100%" justify="center" style={{ minHeight: 400 }} width="100%">
|
||||
<h2>{t('title')}</h2>
|
||||
</Flexbox>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { withSuspense } from '@/components/withSuspense';
|
||||
import { DetailProvider } from '@/features/MCPPluginDetail/DetailProvider';
|
||||
@@ -12,9 +12,9 @@ import { useQuery } from '@/hooks/useQuery';
|
||||
import { useDiscoverStore } from '@/store/discover';
|
||||
import { DiscoverTab } from '@/types/discover';
|
||||
|
||||
import NotFound from '../components/NotFound';
|
||||
import Breadcrumb from '../features/Breadcrumb';
|
||||
import { TocProvider } from '../features/Toc/useToc';
|
||||
import NotFound from '../components/NotFound';
|
||||
import Details from './[slug]/features/Details';
|
||||
import Loading from './[slug]/loading';
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { withSuspense } from '@/components/withSuspense';
|
||||
import { useDiscoverStore } from '@/store/discover';
|
||||
import { DiscoverTab } from '@/types/discover';
|
||||
|
||||
import Breadcrumb from '../features/Breadcrumb';
|
||||
import NotFound from '../components/NotFound';
|
||||
import Breadcrumb from '../features/Breadcrumb';
|
||||
import { DetailProvider } from './[...slugs]/features/DetailProvider';
|
||||
import Details from './[...slugs]/features/Details';
|
||||
import Header from './[...slugs]/features/Header';
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { withSuspense } from '@/components/withSuspense';
|
||||
import { useDiscoverStore } from '@/store/discover';
|
||||
import { DiscoverTab } from '@/types/discover';
|
||||
|
||||
import Breadcrumb from '../features/Breadcrumb';
|
||||
import NotFound from '../components/NotFound';
|
||||
import Breadcrumb from '../features/Breadcrumb';
|
||||
import { DetailProvider } from './[...slugs]/features/DetailProvider';
|
||||
import Details from './[...slugs]/features/Details';
|
||||
import Header from './[...slugs]/features/Header';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { memo, PropsWithChildren } from 'react';
|
||||
import { PropsWithChildren, memo } from 'react';
|
||||
|
||||
import Desktop from './_layout/Desktop';
|
||||
import Mobile from './_layout/Mobile';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { Icon, Tag } from '@lobehub/ui';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import qs from 'query-string';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { SCROLL_PARENT_ID } from '@/app/[variants]/(main)/discover/features/const';
|
||||
import { withSuspense } from '@/components/withSuspense';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { memo, PropsWithChildren } from 'react';
|
||||
import { PropsWithChildren, memo } from 'react';
|
||||
|
||||
import Desktop from './_layout/Desktop';
|
||||
import Mobile from './_layout/Mobile';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { Icon, Tag } from '@lobehub/ui';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import qs from 'query-string';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { SCROLL_PARENT_ID } from '@/app/[variants]/(main)/discover/features/const';
|
||||
import { withSuspense } from '@/components/withSuspense';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { memo, PropsWithChildren } from 'react';
|
||||
import { PropsWithChildren, memo } from 'react';
|
||||
|
||||
import Desktop from './_layout/Desktop';
|
||||
import Mobile from './_layout/Mobile';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { Icon, Tag } from '@lobehub/ui';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import qs from 'query-string';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { SCROLL_PARENT_ID } from '@/app/[variants]/(main)/discover/features/const';
|
||||
import { withSuspense } from '@/components/withSuspense';
|
||||
|
||||
@@ -59,10 +59,7 @@ const Title = memo<TitleProps>(({ tag, children, moreLink, more }) => {
|
||||
title
|
||||
)}
|
||||
{moreLink && (
|
||||
<Link
|
||||
target={moreLink.startsWith('http') ? '_blank' : undefined}
|
||||
to={moreLink}
|
||||
>
|
||||
<Link target={moreLink.startsWith('http') ? '_blank' : undefined} to={moreLink}>
|
||||
<Button className={styles.more} style={{ paddingInline: 6 }} type={'text'}>
|
||||
<span>{more}</span>
|
||||
<Icon icon={ChevronRight} />
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import PanelTitle from '@/components/PanelTitle';
|
||||
|
||||
import FileMenu from './features/FileMenu';
|
||||
import KnowledgeBase from './features/KnowledgeBase';
|
||||
|
||||
const Menu = () => {
|
||||
const { t } = useTranslation('file');
|
||||
|
||||
return (
|
||||
<Flexbox gap={16} height={'100%'}>
|
||||
<Flexbox paddingInline={8}>
|
||||
<PanelTitle desc={t('desc')} title={t('title')} />
|
||||
<FileMenu />
|
||||
</Flexbox>
|
||||
<KnowledgeBase />
|
||||
</Flexbox>
|
||||
);
|
||||
};
|
||||
|
||||
Menu.displayName = 'Menu';
|
||||
|
||||
export default Menu;
|
||||
@@ -1,19 +0,0 @@
|
||||
import { PagePropsWithId } from '@/types/next';
|
||||
|
||||
import FileDetail from './FileDetail';
|
||||
import FilePreview from './FilePreview';
|
||||
import FullscreenModal from './FullscreenModal';
|
||||
|
||||
const Page = async (props: PagePropsWithId) => {
|
||||
const params = await props.params;
|
||||
|
||||
return (
|
||||
<FullscreenModal detail={<FileDetail id={params.id} />}>
|
||||
<FilePreview id={params.id} />
|
||||
</FullscreenModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
export const dynamic = 'force-static';
|
||||
@@ -1,3 +0,0 @@
|
||||
// This ensures that the modal is not rendered when it's not active.
|
||||
|
||||
export default () => null;
|
||||
@@ -1,161 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Icon, Text } from '@lobehub/ui';
|
||||
import { createStyles, useTheme } from 'antd-style';
|
||||
import { Database, FileImage, FileText, FileUpIcon, LibraryBig, SearchCheck } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Center, Flexbox } from 'react-layout-kit';
|
||||
|
||||
import FeatureList from '@/components/FeatureList';
|
||||
import { LOBE_CHAT_CLOUD } from '@/const/branding';
|
||||
import { DATABASE_SELF_HOSTING_URL, OFFICIAL_URL, UTM_SOURCE } from '@/const/url';
|
||||
|
||||
const BLOCK_SIZE = 100;
|
||||
const ICON_SIZE = { size: 72, strokeWidth: 1.5 };
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
actionTitle: css`
|
||||
margin-block-start: 12px;
|
||||
font-size: 16px;
|
||||
color: ${token.colorTextSecondary};
|
||||
`,
|
||||
card: css`
|
||||
cursor: pointer;
|
||||
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
width: 200px;
|
||||
height: 140px;
|
||||
border-radius: ${token.borderRadiusLG}px;
|
||||
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
|
||||
background: ${token.colorFillTertiary};
|
||||
box-shadow: 0 0 0 1px ${token.colorFillTertiary} inset;
|
||||
|
||||
transition: background 0.3s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background: ${token.colorFillSecondary};
|
||||
}
|
||||
`,
|
||||
glow: css`
|
||||
position: absolute;
|
||||
inset-block-end: -12px;
|
||||
inset-inline-end: 0;
|
||||
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
opacity: 0.5;
|
||||
filter: blur(24px);
|
||||
`,
|
||||
|
||||
icon: css`
|
||||
border-radius: ${token.borderRadiusLG}px;
|
||||
color: ${token.colorTextLightSolid};
|
||||
`,
|
||||
iconGroup: css`
|
||||
margin-block-start: -44px;
|
||||
`,
|
||||
}));
|
||||
|
||||
const NotSupportClient = () => {
|
||||
const { t } = useTranslation('file');
|
||||
const theme = useTheme();
|
||||
const { styles } = useStyles();
|
||||
|
||||
const features = [
|
||||
{
|
||||
avatar: Database,
|
||||
desc: t('notSupportGuide.features.allKind.desc'),
|
||||
title: t('notSupportGuide.features.allKind.title'),
|
||||
},
|
||||
{
|
||||
avatar: SearchCheck,
|
||||
desc: t('notSupportGuide.features.embeddings.desc'),
|
||||
title: t('notSupportGuide.features.embeddings.title'),
|
||||
},
|
||||
{
|
||||
avatar: LibraryBig,
|
||||
desc: t('notSupportGuide.features.repos.desc'),
|
||||
title: t('notSupportGuide.features.repos.title'),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Center
|
||||
gap={40}
|
||||
height={'100%'}
|
||||
style={{
|
||||
overflow: 'scroll',
|
||||
}}
|
||||
width={'100%'}
|
||||
>
|
||||
<Flexbox className={styles.iconGroup} gap={12} horizontal>
|
||||
<Center
|
||||
className={styles.icon}
|
||||
height={BLOCK_SIZE * 1.25}
|
||||
style={{
|
||||
background: theme.purple,
|
||||
transform: 'rotateZ(-20deg) translateX(10px)',
|
||||
}}
|
||||
width={BLOCK_SIZE}
|
||||
>
|
||||
<Icon icon={FileImage} size={ICON_SIZE} />
|
||||
</Center>
|
||||
<Center
|
||||
className={styles.icon}
|
||||
height={BLOCK_SIZE * 1.25}
|
||||
style={{
|
||||
background: theme.gold,
|
||||
transform: 'translateY(-22px)',
|
||||
zIndex: 1,
|
||||
}}
|
||||
width={BLOCK_SIZE}
|
||||
>
|
||||
<Icon icon={FileUpIcon} size={ICON_SIZE} />
|
||||
</Center>
|
||||
<Center
|
||||
className={styles.icon}
|
||||
height={BLOCK_SIZE * 1.25}
|
||||
style={{
|
||||
background: theme.geekblue,
|
||||
transform: 'rotateZ(20deg) translateX(-10px)',
|
||||
}}
|
||||
width={BLOCK_SIZE}
|
||||
>
|
||||
<Icon icon={FileText} size={ICON_SIZE} />
|
||||
</Center>
|
||||
</Flexbox>
|
||||
|
||||
<Flexbox justify={'center'} style={{ textAlign: 'center' }}>
|
||||
<Text as={'h1'} style={{ fontSize: 32 }}>
|
||||
{t('notSupportGuide.title')}
|
||||
</Text>
|
||||
<Text type={'secondary'}>
|
||||
<Trans i18nKey={'notSupportGuide.desc'} ns={'file'}>
|
||||
当前部署实例为客户端数据库模式,无法使用文件管理功能。请切换到
|
||||
<Link href={DATABASE_SELF_HOSTING_URL}>服务端数据库部署模式</Link>
|
||||
,或直接使用官方的
|
||||
<Link
|
||||
href={`${OFFICIAL_URL}?utm_source=${UTM_SOURCE}&utm_medium=client_not_support_file`}
|
||||
>
|
||||
{LOBE_CHAT_CLOUD}
|
||||
</Link>
|
||||
</Trans>
|
||||
</Text>
|
||||
</Flexbox>
|
||||
|
||||
<Flexbox style={{ marginTop: 40 }}>
|
||||
<FeatureList data={features} />
|
||||
</Flexbox>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotSupportClient;
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import FilePanel from '@/features/FileSidePanel';
|
||||
|
||||
import { LayoutProps } from '../type';
|
||||
import Container from './Container';
|
||||
import RegisterHotkeys from './RegisterHotkeys';
|
||||
|
||||
const Layout = ({ children, menu, modal }: LayoutProps) => {
|
||||
return (
|
||||
<>
|
||||
<Flexbox
|
||||
height={'100%'}
|
||||
horizontal
|
||||
style={{ maxWidth: '100%', overflow: 'hidden', position: 'relative' }}
|
||||
width={'100%'}
|
||||
>
|
||||
<FilePanel>{menu}</FilePanel>
|
||||
<Container>{children}</Container>
|
||||
</Flexbox>
|
||||
<RegisterHotkeys />
|
||||
{modal}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Layout.displayName = 'DesktopFileLayout';
|
||||
|
||||
export default Layout;
|
||||
@@ -1,47 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { createStyles } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { useShowMobileWorkspace } from '@/hooks/useShowMobileWorkspace';
|
||||
|
||||
import { LayoutProps } from './type';
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
main: css`
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: ${token.colorBgLayout};
|
||||
`,
|
||||
}));
|
||||
|
||||
const Layout = memo<LayoutProps>(({ children, menu }) => {
|
||||
const showMobileWorkspace = useShowMobileWorkspace();
|
||||
const { styles } = useStyles();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flexbox
|
||||
className={styles.main}
|
||||
height="100%"
|
||||
style={showMobileWorkspace ? { display: 'none' } : undefined}
|
||||
width="100%"
|
||||
>
|
||||
{menu}
|
||||
</Flexbox>
|
||||
<Flexbox
|
||||
className={styles.main}
|
||||
height="100%"
|
||||
style={showMobileWorkspace ? undefined : { display: 'none' }}
|
||||
width="100%"
|
||||
>
|
||||
{children}
|
||||
</Flexbox>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
Layout.displayName = 'MobileChatLayout';
|
||||
|
||||
export default Layout;
|
||||
@@ -1,7 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export interface LayoutProps {
|
||||
children: ReactNode;
|
||||
menu: ReactNode;
|
||||
modal: ReactNode;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import ServerLayout from '@/components/server/ServerLayout';
|
||||
import { isServerMode } from '@/const/version';
|
||||
|
||||
import NotSupportClient from './NotSupportClient';
|
||||
import Desktop from './_layout/Desktop';
|
||||
import Mobile from './_layout/Mobile';
|
||||
import { LayoutProps } from './_layout/type';
|
||||
|
||||
const Layout = ServerLayout<LayoutProps>({ Desktop, Mobile });
|
||||
|
||||
Layout.displayName = 'FileLayout';
|
||||
|
||||
export default (props: LayoutProps) => {
|
||||
// if there is client db mode , tell user to switch to server mode
|
||||
if (!isServerMode) return <NotSupportClient />;
|
||||
|
||||
return <Layout {...props} />;
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useFileCategory } from '@/app/[variants]/(main)/files/hooks/useFileCategory';
|
||||
import FileManager from '@/features/FileManager';
|
||||
import { FilesTabs } from '@/types/files';
|
||||
|
||||
export default () => {
|
||||
const { t } = useTranslation('file');
|
||||
const [category] = useFileCategory();
|
||||
|
||||
return <FileManager category={category} title={t(`tab.${category as FilesTabs}`)} />;
|
||||
};
|
||||
@@ -1,63 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { ActionIcon, Button, Text } from '@lobehub/ui';
|
||||
import { Divider } from 'antd';
|
||||
import { useTheme } from 'antd-style';
|
||||
import { ArrowLeftIcon, DownloadIcon } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { downloadFile } from '@/utils/client/downloadFile';
|
||||
|
||||
interface HeaderProps {
|
||||
filename: string;
|
||||
id: string;
|
||||
url: string;
|
||||
}
|
||||
const Header = memo<HeaderProps>(({ filename, url }) => {
|
||||
const { t } = useTranslation('common');
|
||||
const router = useRouter();
|
||||
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
horizontal
|
||||
justify={'space-between'}
|
||||
paddingBlock={12}
|
||||
paddingInline={12}
|
||||
style={{ borderBottom: `1px solid ${theme.colorSplit}` }}
|
||||
>
|
||||
<Flexbox align={'baseline'} horizontal>
|
||||
<Button
|
||||
icon={ArrowLeftIcon}
|
||||
onClick={() => {
|
||||
router.push('/files');
|
||||
}}
|
||||
size={'small'}
|
||||
type={'text'}
|
||||
>
|
||||
{t('back')}
|
||||
</Button>
|
||||
<Divider type={'vertical'} />
|
||||
<Text
|
||||
as={'h1'}
|
||||
style={{ fontSize: 16, lineHeight: 1.5, marginBottom: 0, paddingInlineStart: 8 }}
|
||||
>
|
||||
{filename}
|
||||
</Text>
|
||||
</Flexbox>
|
||||
<Flexbox>
|
||||
<ActionIcon
|
||||
icon={DownloadIcon}
|
||||
onClick={() => {
|
||||
downloadFile(url, filename);
|
||||
}}
|
||||
/>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
export default Header;
|
||||
@@ -1,41 +0,0 @@
|
||||
import { getUserAuth } from '@lobechat/utils/server';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import FileViewer from '@/features/FileViewer';
|
||||
import { createCallerFactory } from '@/libs/trpc/lambda';
|
||||
import { lambdaRouter } from '@/server/routers/lambda';
|
||||
import { PagePropsWithId } from '@/types/next';
|
||||
|
||||
import FileDetail from '../features/FileDetail';
|
||||
import Header from './Header';
|
||||
|
||||
const createCaller = createCallerFactory(lambdaRouter);
|
||||
|
||||
const FilePage = async (props: PagePropsWithId) => {
|
||||
const params = await props.params;
|
||||
|
||||
const { userId } = await getUserAuth();
|
||||
|
||||
const caller = createCaller({ userId });
|
||||
|
||||
const file = await caller.file.getFileItemById({ id: params.id });
|
||||
|
||||
if (!file) return notFound();
|
||||
|
||||
return (
|
||||
<Flexbox horizontal width={'100%'}>
|
||||
<Flexbox flex={1}>
|
||||
<Flexbox height={'100%'}>
|
||||
<Header filename={file.name} id={params.id} url={file.url} />
|
||||
<Flexbox height={'100%'} style={{ overflow: 'scroll' }}>
|
||||
<FileViewer {...file} />
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
<FileDetail {...file} />
|
||||
</Flexbox>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilePage;
|
||||
@@ -1,6 +0,0 @@
|
||||
import { useQueryState } from 'nuqs';
|
||||
|
||||
import { FilesTabs } from '@/types/files';
|
||||
|
||||
export const useFileCategory = () =>
|
||||
useQueryState('category', { clearOnDefault: true, defaultValue: FilesTabs.All });
|
||||
@@ -1,3 +0,0 @@
|
||||
import Loading from '@/components/Loading/BrandTextLoading';
|
||||
|
||||
export default () => <Loading />;
|
||||
@@ -0,0 +1,73 @@
|
||||
'use client';
|
||||
|
||||
import { App } from 'antd';
|
||||
import { memo, useEffect } from 'react';
|
||||
import { MemoryRouter, Navigate, Route, Routes, useLocation } from 'react-router-dom';
|
||||
|
||||
import KnowledgeBaseDetailPage from './routes/KnowledgeBaseDetail';
|
||||
import KnowledgeBasesListPage from './routes/KnowledgeBasesList';
|
||||
import KnowledgeHomePage from './routes/KnowledgeHome';
|
||||
|
||||
// Get initial path from URL
|
||||
const getInitialPath = () => {
|
||||
if (typeof window === 'undefined') return '/';
|
||||
const fullPath = window.location.pathname;
|
||||
const searchParams = window.location.search;
|
||||
const knowledgeIndex = fullPath.indexOf('/knowledge');
|
||||
|
||||
if (knowledgeIndex !== -1) {
|
||||
const pathAfterKnowledge = fullPath.slice(knowledgeIndex + '/knowledge'.length) || '/';
|
||||
return pathAfterKnowledge + searchParams;
|
||||
}
|
||||
return '/';
|
||||
};
|
||||
|
||||
// Helper component to sync URL with MemoryRouter
|
||||
const UrlSynchronizer = () => {
|
||||
const location = useLocation();
|
||||
|
||||
// Update browser URL when location changes
|
||||
useEffect(() => {
|
||||
const newUrl = `/knowledge${location.pathname}${location.search}`;
|
||||
if (window.location.pathname + window.location.search !== newUrl) {
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
}
|
||||
}, [location.pathname, location.search]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Main Knowledge Router component with MemoryRouter
|
||||
* This serves as the entry point for all knowledge-related routes
|
||||
* Uses MemoryRouter with URL synchronization to support query parameters like ?file=[id]
|
||||
*
|
||||
* Route structure:
|
||||
* - / → Knowledge home (file list with categories)
|
||||
* - /bases → Knowledge bases list
|
||||
* - /bases/:id → Knowledge base detail (file list for specific base)
|
||||
*/
|
||||
const KnowledgeRouter = memo(() => {
|
||||
return (
|
||||
<App style={{ display: 'flex', flex: 1, height: '100%' }}>
|
||||
<MemoryRouter initialEntries={[getInitialPath()]} initialIndex={0}>
|
||||
<UrlSynchronizer />
|
||||
<Routes>
|
||||
{/* Knowledge home - file list page */}
|
||||
<Route element={<KnowledgeHomePage />} path="/" />
|
||||
|
||||
{/* Knowledge bases routes */}
|
||||
<Route element={<KnowledgeBasesListPage />} path="/bases" />
|
||||
<Route element={<KnowledgeBaseDetailPage />} path="/bases/:id" />
|
||||
|
||||
{/* Fallback */}
|
||||
<Route element={<Navigate replace to="/" />} path="*" />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</App>
|
||||
);
|
||||
});
|
||||
|
||||
KnowledgeRouter.displayName = 'KnowledgeRouter';
|
||||
|
||||
export default KnowledgeRouter;
|
||||
@@ -0,0 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import { BrandTextLoading } from '@/components/Loading';
|
||||
|
||||
const KnowledgeRouter = dynamic(() => import('../KnowledgeRouter'), {
|
||||
loading: BrandTextLoading,
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export default KnowledgeRouter;
|
||||
+15
-3
@@ -1,21 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { Icon, Tag } from '@lobehub/ui';
|
||||
import { ActionIcon, Icon, Tag } from '@lobehub/ui';
|
||||
import { Descriptions, Divider } from 'antd';
|
||||
import { useTheme } from 'antd-style';
|
||||
import dayjs from 'dayjs';
|
||||
import { BoltIcon } from 'lucide-react';
|
||||
import { BoltIcon, DownloadIcon } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { FileListItem } from '@/types/files';
|
||||
import { downloadFile } from '@/utils/client/downloadFile';
|
||||
import { formatSize } from '@/utils/format';
|
||||
|
||||
export const DETAIL_PANEL_WIDTH = 300;
|
||||
|
||||
const FileDetail = memo<FileListItem>((props) => {
|
||||
const { name, embeddingStatus, size, createdAt, updatedAt, chunkCount } = props || {};
|
||||
const { name, embeddingStatus, size, createdAt, updatedAt, chunkCount, url } = props || {};
|
||||
const { t } = useTranslation('file');
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -73,6 +74,17 @@ const FileDetail = memo<FileListItem>((props) => {
|
||||
<Descriptions
|
||||
colon={false}
|
||||
column={1}
|
||||
extra={
|
||||
url && (
|
||||
<ActionIcon
|
||||
icon={DownloadIcon}
|
||||
onClick={() => {
|
||||
downloadFile(url, name);
|
||||
}}
|
||||
title={t('download', { ns: 'common' })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
items={items}
|
||||
labelStyle={{ width: 120 }}
|
||||
size={'small'}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Icon } from '@lobehub/ui';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => {
|
||||
return {
|
||||
container: css`
|
||||
cursor: pointer;
|
||||
|
||||
width: fit-content;
|
||||
height: 24px;
|
||||
padding-inline: 8px;
|
||||
border-radius: 6px;
|
||||
|
||||
color: ${token.colorTextTertiary};
|
||||
|
||||
&:hover {
|
||||
color: ${token.colorTextSecondary};
|
||||
background: ${token.colorFillTertiary};
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
interface GoBackProps {
|
||||
/**
|
||||
* The path to navigate to (relative to MemoryRouter)
|
||||
* e.g., "/" for /knowledge, "/bases" for /knowledge/bases
|
||||
*/
|
||||
to: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GoBack component for react-router-dom
|
||||
* Uses useNavigate instead of Next.js Link
|
||||
*/
|
||||
const GoBack = memo<GoBackProps>(({ to }) => {
|
||||
const { t } = useTranslation('components');
|
||||
const { styles } = useStyles();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleClick = () => {
|
||||
navigate(to);
|
||||
};
|
||||
|
||||
return (
|
||||
<Flexbox align={'center'} className={styles.container} gap={4} horizontal onClick={handleClick}>
|
||||
<Icon icon={ArrowLeft} />
|
||||
<div>{t('GoBack.back')}</div>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
GoBack.displayName = 'GoBack';
|
||||
|
||||
export default GoBack;
|
||||
+3
-1
@@ -93,7 +93,7 @@ const Content = memo<KnowledgeBaseItemProps>(({ id, name, showMore }) => {
|
||||
},
|
||||
},
|
||||
],
|
||||
[],
|
||||
[t, id, modal, removeKnowledgeBase],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -172,4 +172,6 @@ const Content = memo<KnowledgeBaseItemProps>(({ id, name, showMore }) => {
|
||||
);
|
||||
});
|
||||
|
||||
Content.displayName = 'KnowledgeBaseItemContent';
|
||||
|
||||
export default Content;
|
||||
+22
-23
@@ -1,9 +1,7 @@
|
||||
import { createStyles } from 'antd-style';
|
||||
import Link from 'next/link';
|
||||
import React, { memo, useState } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { useQueryRoute } from '@/hooks/useQueryRoute';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Content, { knowledgeItemClass } from './Content';
|
||||
|
||||
@@ -46,31 +44,32 @@ export interface KnowledgeBaseItemProps {
|
||||
const KnowledgeBaseItem = memo<KnowledgeBaseItemProps>(({ name, active, id }) => {
|
||||
const { styles, cx } = useStyles();
|
||||
const [isHover, setHovering] = useState(false);
|
||||
const router = useQueryRoute();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
router.push(`/repos/${id}`);
|
||||
const handleClick = () => {
|
||||
navigate(`/bases/${id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Link href={`/repos/${id}`} onClick={handleClick}>
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
className={cx(styles.container, knowledgeItemClass, active && styles.active)}
|
||||
distribution={'space-between'}
|
||||
horizontal
|
||||
onMouseEnter={() => {
|
||||
setHovering(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHovering(false);
|
||||
}}
|
||||
>
|
||||
<Content id={id} name={name} showMore={isHover} />
|
||||
</Flexbox>
|
||||
</Link>
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
className={cx(styles.container, knowledgeItemClass, active && styles.active)}
|
||||
distribution={'space-between'}
|
||||
horizontal
|
||||
onClick={handleClick}
|
||||
onMouseEnter={() => {
|
||||
setHovering(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHovering(false);
|
||||
}}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Content id={id} name={name} showMore={isHover} />
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
KnowledgeBaseItem.displayName = 'KnowledgeBaseItem';
|
||||
|
||||
export default KnowledgeBaseItem;
|
||||
+4
-2
@@ -4,8 +4,8 @@ import { Virtuoso } from 'react-virtuoso';
|
||||
|
||||
import { useKnowledgeBaseStore } from '@/store/knowledgeBase';
|
||||
|
||||
import KnowledgeBaseItem from '../KnowledgeBaseItem';
|
||||
import EmptyStatus from './EmptyStatus';
|
||||
import Item from './Item';
|
||||
import { SkeletonList } from './SkeletonList';
|
||||
|
||||
const KnowledgeBaseList = () => {
|
||||
@@ -21,7 +21,9 @@ const KnowledgeBaseList = () => {
|
||||
<Virtuoso
|
||||
data={data}
|
||||
fixedItemHeight={36}
|
||||
itemContent={(index, data) => <Item id={data.id} key={data.id} name={data.name} />}
|
||||
itemContent={(index, item) => (
|
||||
<KnowledgeBaseItem id={item.id} key={item.id} name={item.name} />
|
||||
)}
|
||||
/>
|
||||
</Flexbox>
|
||||
);
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { memo } from 'react';
|
||||
|
||||
import { fileManagerSelectors, useFileStore } from '@/store/file';
|
||||
|
||||
import Detail from '../../../features/FileDetail';
|
||||
import Detail from '../FileDetail';
|
||||
|
||||
const FileDetail = memo<{ id: string }>(({ id }) => {
|
||||
const file = useFileStore(fileManagerSelectors.getFileById(id));
|
||||
+10
-9
@@ -3,10 +3,9 @@
|
||||
import { Modal } from '@lobehub/ui';
|
||||
import { ConfigProvider } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { ReactNode, useCallback, useState } from 'react';
|
||||
|
||||
import { DETAIL_PANEL_WIDTH } from '@/app/[variants]/(main)/files/features/FileDetail';
|
||||
import { DETAIL_PANEL_WIDTH } from '../FileDetail';
|
||||
|
||||
const useStyles = createStyles(({ css, token }, showDetail: boolean) => {
|
||||
return {
|
||||
@@ -53,14 +52,19 @@ const useStyles = createStyles(({ css, token }, showDetail: boolean) => {
|
||||
interface FullscreenModalProps {
|
||||
children: ReactNode;
|
||||
detail?: ReactNode;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const FullscreenModal = ({ children, detail }: FullscreenModalProps) => {
|
||||
const router = useRouter();
|
||||
const FullscreenModal = ({ children, detail, onClose }: FullscreenModalProps) => {
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
const { styles } = useStyles(!!detail);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
setOpen(false);
|
||||
onClose?.();
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfigProvider theme={{ token: { motion: false } }}>
|
||||
@@ -68,10 +72,7 @@ const FullscreenModal = ({ children, detail }: FullscreenModalProps) => {
|
||||
className={styles.modal}
|
||||
classNames={{ body: styles.body, content: styles.content, header: styles.header }}
|
||||
footer={false}
|
||||
onCancel={() => {
|
||||
router.back();
|
||||
setOpen(false);
|
||||
}}
|
||||
onCancel={handleCancel}
|
||||
open={open}
|
||||
width={'auto'}
|
||||
>
|
||||
@@ -0,0 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import FileDetail from './FileDetail';
|
||||
import FilePreview from './FilePreview';
|
||||
import FullscreenModal from './FullscreenModal';
|
||||
|
||||
interface ModalPageClientProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const ModalPageClient = ({ id }: ModalPageClientProps) => {
|
||||
const router = useRouter();
|
||||
const handleClose = useCallback(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const { pathname, search } = window.location;
|
||||
const basePath = pathname.replace(/\/modal\/?$/, '');
|
||||
|
||||
router.replace(`${basePath || '/'}${search}`);
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<FullscreenModal detail={<FileDetail id={id} />} onClose={handleClose}>
|
||||
<FilePreview id={id} />
|
||||
</FullscreenModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalPageClient;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { PagePropsWithId } from '@/types/next';
|
||||
|
||||
import ModalPageClient from './ModalPageClient';
|
||||
|
||||
const Page = async (props: PagePropsWithId) => {
|
||||
const params = await props.params;
|
||||
|
||||
return <ModalPageClient id={params.id} />;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
export const dynamic = 'force-static';
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const FILE_MODAL_QUERY_KEY = 'files';
|
||||
const FILE_MODAL_QUERY_EVENT = 'lobe-files-querychange';
|
||||
|
||||
const getCurrentSearch = () => {
|
||||
if (typeof window === 'undefined') return '';
|
||||
return window.location.search;
|
||||
};
|
||||
|
||||
export const getCurrentFileModalId = () => {
|
||||
const search = getCurrentSearch();
|
||||
if (!search) return undefined;
|
||||
|
||||
const params = new URLSearchParams(search);
|
||||
return params.get(FILE_MODAL_QUERY_KEY) ?? undefined;
|
||||
};
|
||||
|
||||
const pushStateWithParams = (params: URLSearchParams) => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const search = params.toString();
|
||||
const hash = window.location.hash;
|
||||
const pathname = window.location.pathname;
|
||||
const url = `${pathname}${search ? `?${search}` : ''}${hash}`;
|
||||
|
||||
window.history.pushState({}, '', url);
|
||||
window.dispatchEvent(new Event(FILE_MODAL_QUERY_EVENT));
|
||||
};
|
||||
|
||||
export const setFileModalId = (id?: string) => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const params = new URLSearchParams(getCurrentSearch());
|
||||
|
||||
if (!id) {
|
||||
params.delete(FILE_MODAL_QUERY_KEY);
|
||||
} else {
|
||||
params.set(FILE_MODAL_QUERY_KEY, id);
|
||||
}
|
||||
|
||||
pushStateWithParams(params);
|
||||
};
|
||||
|
||||
export const useFileModalId = (): string | undefined => {
|
||||
const [fileId, setFileId] = useState<string | undefined>(() => getCurrentFileModalId());
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const handler = () => {
|
||||
setFileId(getCurrentFileModalId());
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', handler);
|
||||
window.addEventListener(FILE_MODAL_QUERY_EVENT, handler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('popstate', handler);
|
||||
window.removeEventListener(FILE_MODAL_QUERY_EVENT, handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return fileId;
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { FilesTabs } from '@/types/files';
|
||||
|
||||
/**
|
||||
* Hook to manage file category filter in URL search params
|
||||
* Uses react-router-dom instead of nuqs for MemoryRouter compatibility
|
||||
*/
|
||||
export const useFileCategory = (): [string, (value: string) => void] => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const category = searchParams.get('category') ?? FilesTabs.All;
|
||||
|
||||
const setCategory = (value: string) => {
|
||||
setSearchParams(
|
||||
(prev) => {
|
||||
const newParams = new URLSearchParams(prev);
|
||||
|
||||
if (value === FilesTabs.All) {
|
||||
// Clear on default
|
||||
newParams.delete('category');
|
||||
} else {
|
||||
newParams.set('category', value);
|
||||
}
|
||||
|
||||
return newParams;
|
||||
},
|
||||
{ replace: true },
|
||||
);
|
||||
};
|
||||
|
||||
return [category, setCategory];
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import FileModalQueryRoute from '@/app/[variants]/(main)/knowledge/shared/FileModalQueryRoute';
|
||||
import { useSetFileModalId } from '@/app/[variants]/(main)/knowledge/shared/useFileQueryParam';
|
||||
import FileManager from '@/features/FileManager';
|
||||
import FilePanel from '@/features/FileSidePanel';
|
||||
import { knowledgeBaseSelectors, useKnowledgeBaseStore } from '@/store/knowledgeBase';
|
||||
|
||||
import { useKnowledgeBaseItem } from '../../hooks/useKnowledgeItem';
|
||||
import Menu from './menu/Menu';
|
||||
|
||||
/**
|
||||
* Knowledge Base Detail Page
|
||||
* Shows file list for a specific knowledge base
|
||||
* Supports ?file=[fileId] query param for file preview modal
|
||||
*/
|
||||
const KnowledgeBaseDetailPage = memo(() => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const setFileModalId = useSetFileModalId();
|
||||
|
||||
useKnowledgeBaseItem(id!);
|
||||
const name = useKnowledgeBaseStore(knowledgeBaseSelectors.getKnowledgeBaseNameById(id!));
|
||||
|
||||
if (!id) {
|
||||
return <div>Knowledge base ID is required</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilePanel>
|
||||
<Menu id={id} />
|
||||
</FilePanel>
|
||||
<Flexbox flex={1} style={{ overflow: 'hidden', position: 'relative' }}>
|
||||
<FileManager knowledgeBaseId={id} onOpenFile={setFileModalId} title={name} />
|
||||
</Flexbox>
|
||||
<FileModalQueryRoute />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
KnowledgeBaseDetailPage.displayName = 'KnowledgeBaseDetailPage';
|
||||
|
||||
export default KnowledgeBaseDetailPage;
|
||||
+6
-2
@@ -5,16 +5,17 @@ import { Skeleton } from 'antd';
|
||||
import { memo } from 'react';
|
||||
import { Center, Flexbox } from 'react-layout-kit';
|
||||
|
||||
import GoBack from '@/components/GoBack';
|
||||
import RepoIcon from '@/components/RepoIcon';
|
||||
import { knowledgeBaseSelectors, useKnowledgeBaseStore } from '@/store/knowledgeBase';
|
||||
|
||||
import GoBack from '../../../components/GoBack';
|
||||
|
||||
const Head = memo<{ id: string }>(({ id }) => {
|
||||
const name = useKnowledgeBaseStore(knowledgeBaseSelectors.getKnowledgeBaseNameById(id));
|
||||
|
||||
return (
|
||||
<Flexbox gap={8}>
|
||||
<GoBack href={'/files'} />
|
||||
<GoBack to="/" />
|
||||
<Flexbox align={'center'} gap={8} height={36} horizontal>
|
||||
<Center style={{ minWidth: 24 }} width={24}>
|
||||
<RepoIcon />
|
||||
@@ -31,4 +32,7 @@ const Head = memo<{ id: string }>(({ id }) => {
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
Head.displayName = 'Head';
|
||||
|
||||
export default Head;
|
||||
+7
-5
@@ -1,18 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import Head from './Head';
|
||||
import Menu from './Menu';
|
||||
import MenuItems from './MenuItems';
|
||||
|
||||
const ComposeMenu = memo<{ id: string }>(({ id }) => {
|
||||
const Menu = memo<{ id: string }>(({ id }) => {
|
||||
return (
|
||||
<Flexbox gap={16} height={'100%'} paddingInline={12} style={{ paddingTop: 12 }}>
|
||||
<Head id={id} />
|
||||
<Menu />
|
||||
<MenuItems />
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
ComposeMenu.displayName = 'ComposeMenu';
|
||||
Menu.displayName = 'Menu';
|
||||
|
||||
export default ComposeMenu;
|
||||
export default Menu;
|
||||
@@ -0,0 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import { Icon } from '@lobehub/ui';
|
||||
import { FileText } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import Menu from '@/components/Menu';
|
||||
import type { MenuProps } from '@/components/Menu';
|
||||
|
||||
const MenuItems = () => {
|
||||
const { t } = useTranslation('knowledgeBase');
|
||||
|
||||
const items: MenuProps['items'] = useMemo(
|
||||
() => [
|
||||
{
|
||||
icon: <Icon icon={FileText} />,
|
||||
key: 'files',
|
||||
label: t('tab.files'),
|
||||
},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
return (
|
||||
<Flexbox>
|
||||
<Menu compact items={items} selectable selectedKeys={['files']} />
|
||||
</Flexbox>
|
||||
);
|
||||
};
|
||||
|
||||
MenuItems.displayName = 'MenuItems';
|
||||
|
||||
export default MenuItems;
|
||||
@@ -0,0 +1,37 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import FilePanel from '@/features/FileSidePanel';
|
||||
|
||||
import Menu from '../KnowledgeBaseDetail/menu/Menu';
|
||||
|
||||
/**
|
||||
* Knowledge Base Settings Page
|
||||
* Configuration page for a specific knowledge base
|
||||
*/
|
||||
const KnowledgeBaseSettingsPage = memo(() => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
if (!id) {
|
||||
return <div>Knowledge base ID is required</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilePanel>
|
||||
<Menu id={id} />
|
||||
</FilePanel>
|
||||
<Flexbox align="center" flex={1} justify="center" style={{ overflow: 'hidden' }}>
|
||||
{/* TODO: Add settings form components here */}
|
||||
<div>Settings page for knowledge base: {id}</div>
|
||||
</Flexbox>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
KnowledgeBaseSettingsPage.displayName = 'KnowledgeBaseSettingsPage';
|
||||
|
||||
export default KnowledgeBaseSettingsPage;
|
||||
@@ -0,0 +1,41 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import PanelTitle from '@/components/PanelTitle';
|
||||
import FilePanel from '@/features/FileSidePanel';
|
||||
|
||||
import KnowledgeBaseList from '../../components/KnowledgeBaseList';
|
||||
|
||||
/**
|
||||
* Knowledge Bases List Page
|
||||
* Shows all available knowledge bases
|
||||
*/
|
||||
const KnowledgeBasesListPage = memo(() => {
|
||||
const { t } = useTranslation('file');
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilePanel>
|
||||
<Flexbox gap={16} height={'100%'} paddingInline={8}>
|
||||
<PanelTitle title={t('knowledgeBase.title')} />
|
||||
<KnowledgeBaseList />
|
||||
</Flexbox>
|
||||
</FilePanel>
|
||||
<Flexbox
|
||||
align="center"
|
||||
flex={1}
|
||||
justify="center"
|
||||
style={{ overflow: 'hidden', position: 'relative' }}
|
||||
>
|
||||
<div>Select a knowledge base to view details</div>
|
||||
</Flexbox>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
KnowledgeBasesListPage.displayName = 'KnowledgeBasesListPage';
|
||||
|
||||
export default KnowledgeBasesListPage;
|
||||
@@ -0,0 +1,131 @@
|
||||
'use client';
|
||||
|
||||
import { createStyles } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
|
||||
import NProgress from '@/components/NProgress';
|
||||
import PanelTitle from '@/components/PanelTitle';
|
||||
import FileManager from '@/features/FileManager';
|
||||
import FilePanel from '@/features/FileSidePanel';
|
||||
import { useShowMobileWorkspace } from '@/hooks/useShowMobileWorkspace';
|
||||
import { FilesTabs } from '@/types/files';
|
||||
|
||||
import { useFileCategory } from '../../hooks/useFileCategory';
|
||||
import FileModalQueryRoute from '../../shared/FileModalQueryRoute';
|
||||
import { useSetFileModalId } from '../../shared/useFileQueryParam';
|
||||
import Container from './layout/Container';
|
||||
import RegisterHotkeys from './layout/RegisterHotkeys';
|
||||
import FileMenu from './menu/FileMenu';
|
||||
import KnowledgeBase from './menu/KnowledgeBase';
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
main: css`
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: ${token.colorBgLayout};
|
||||
`,
|
||||
}));
|
||||
|
||||
// Menu content component
|
||||
const MenuContent = memo(() => {
|
||||
const { t } = useTranslation('file');
|
||||
|
||||
return (
|
||||
<Flexbox gap={16} height={'100%'}>
|
||||
<Flexbox paddingInline={8}>
|
||||
<PanelTitle desc={t('desc')} title={t('title')} />
|
||||
<FileMenu />
|
||||
</Flexbox>
|
||||
<KnowledgeBase />
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
MenuContent.displayName = 'MenuContent';
|
||||
|
||||
// Main files list component
|
||||
const FilesListPage = memo(() => {
|
||||
const [category] = useFileCategory();
|
||||
const setFileModalId = useSetFileModalId();
|
||||
|
||||
return (
|
||||
<FileManager
|
||||
category={category}
|
||||
onOpenFile={setFileModalId}
|
||||
title={`${category as FilesTabs}`}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
FilesListPage.displayName = 'FilesListPage';
|
||||
|
||||
// Desktop layout
|
||||
const DesktopLayout = memo(() => {
|
||||
return (
|
||||
<>
|
||||
<NProgress />
|
||||
<Flexbox
|
||||
height={'100%'}
|
||||
horizontal
|
||||
style={{ maxWidth: '100%', overflow: 'hidden', position: 'relative' }}
|
||||
width={'100%'}
|
||||
>
|
||||
<FilePanel>
|
||||
<MenuContent />
|
||||
</FilePanel>
|
||||
<Container>
|
||||
<FilesListPage />
|
||||
</Container>
|
||||
</Flexbox>
|
||||
<RegisterHotkeys />
|
||||
<FileModalQueryRoute />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
DesktopLayout.displayName = 'DesktopLayout';
|
||||
|
||||
// Mobile layout
|
||||
const MobileLayout = memo(() => {
|
||||
const showMobileWorkspace = useShowMobileWorkspace();
|
||||
const { styles } = useStyles();
|
||||
|
||||
return (
|
||||
<>
|
||||
<NProgress />
|
||||
<Flexbox
|
||||
className={styles.main}
|
||||
height="100%"
|
||||
style={showMobileWorkspace ? { display: 'none' } : undefined}
|
||||
width="100%"
|
||||
>
|
||||
<MenuContent />
|
||||
</Flexbox>
|
||||
<Flexbox
|
||||
className={styles.main}
|
||||
height="100%"
|
||||
style={showMobileWorkspace ? undefined : { display: 'none' }}
|
||||
width="100%"
|
||||
>
|
||||
<FilesListPage />
|
||||
</Flexbox>
|
||||
<FileModalQueryRoute />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
MobileLayout.displayName = 'MobileLayout';
|
||||
|
||||
// Main Knowledge Home Page
|
||||
const KnowledgeHomePage = memo(() => {
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 });
|
||||
|
||||
return isMobile ? <MobileLayout /> : <DesktopLayout />;
|
||||
});
|
||||
|
||||
KnowledgeHomePage.displayName = 'KnowledgeHomePage';
|
||||
|
||||
export default KnowledgeHomePage;
|
||||
+2
@@ -21,4 +21,6 @@ const Container = memo<PropsWithChildren>(({ children }) => {
|
||||
);
|
||||
});
|
||||
|
||||
Container.displayName = 'Container';
|
||||
|
||||
export default Container;
|
||||
+10
-32
@@ -2,16 +2,16 @@
|
||||
|
||||
import { Icon } from '@lobehub/ui';
|
||||
import { FileText, Globe, ImageIcon, LayoutGrid, Mic2, SquarePlay } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { useFileCategory } from '@/app/[variants]/(main)/files/hooks/useFileCategory';
|
||||
import Menu from '@/components/Menu';
|
||||
import type { MenuProps } from '@/components/Menu';
|
||||
import { FilesTabs } from '@/types/files';
|
||||
|
||||
import { useFileCategory } from '../../../hooks/useFileCategory';
|
||||
|
||||
const FileMenu = memo(() => {
|
||||
const { t } = useTranslation('file');
|
||||
const [activeKey, setActiveKey] = useFileCategory();
|
||||
@@ -22,56 +22,32 @@ const FileMenu = memo(() => {
|
||||
{
|
||||
icon: <Icon icon={LayoutGrid} />,
|
||||
key: FilesTabs.All,
|
||||
label: (
|
||||
<Link href={'/files'} onClick={(e) => e.preventDefault()}>
|
||||
{t('tab.all')}
|
||||
</Link>
|
||||
),
|
||||
label: t('tab.all'),
|
||||
},
|
||||
{
|
||||
icon: <Icon icon={FileText} />,
|
||||
key: FilesTabs.Documents,
|
||||
label: (
|
||||
<Link href={'/files?category=documents'} onClick={(e) => e.preventDefault()}>
|
||||
{t('tab.documents')}
|
||||
</Link>
|
||||
),
|
||||
label: t('tab.documents'),
|
||||
},
|
||||
{
|
||||
icon: <Icon icon={ImageIcon} />,
|
||||
key: FilesTabs.Images,
|
||||
label: (
|
||||
<Link href={'/files?category=images'} onClick={(e) => e.preventDefault()}>
|
||||
{t('tab.images')}
|
||||
</Link>
|
||||
),
|
||||
label: t('tab.images'),
|
||||
},
|
||||
{
|
||||
icon: <Icon icon={Mic2} />,
|
||||
key: FilesTabs.Audios,
|
||||
label: (
|
||||
<Link href={'/files?category=audios'} onClick={(e) => e.preventDefault()}>
|
||||
{t('tab.audios')}
|
||||
</Link>
|
||||
),
|
||||
label: t('tab.audios'),
|
||||
},
|
||||
{
|
||||
icon: <Icon icon={SquarePlay} />,
|
||||
key: FilesTabs.Videos,
|
||||
label: (
|
||||
<Link href={'/files?category=videos'} onClick={(e) => e.preventDefault()}>
|
||||
{t('tab.videos')}
|
||||
</Link>
|
||||
),
|
||||
label: t('tab.videos'),
|
||||
},
|
||||
{
|
||||
icon: <Icon icon={Globe} />,
|
||||
key: FilesTabs.Websites,
|
||||
label: (
|
||||
<Link href={'/files?category=websites'} onClick={(e) => e.preventDefault()}>
|
||||
{t('tab.websites')}
|
||||
</Link>
|
||||
),
|
||||
label: t('tab.websites'),
|
||||
},
|
||||
]
|
||||
.filter(Boolean)
|
||||
@@ -94,4 +70,6 @@ const FileMenu = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
FileMenu.displayName = 'FileMenu';
|
||||
|
||||
export default FileMenu;
|
||||
+17
-2
@@ -5,10 +5,11 @@ import { PlusIcon } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useCreateNewModal } from '@/features/KnowledgeBaseModal';
|
||||
|
||||
import KnowledgeBaseList from './KnowledgeBaseList';
|
||||
import KnowledgeBaseList from '../../../components/KnowledgeBaseList';
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
header: css`
|
||||
@@ -19,11 +20,20 @@ const useStyles = createStyles(({ css, token }) => ({
|
||||
const KnowledgeBase = () => {
|
||||
const { t } = useTranslation('file');
|
||||
const { styles } = useStyles();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [showList, setShowList] = useState(true);
|
||||
|
||||
const { open } = useCreateNewModal();
|
||||
|
||||
const handleCreate = () => {
|
||||
open({
|
||||
onSuccess: (id) => {
|
||||
navigate(`/bases/${id}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Flexbox flex={1} gap={8}>
|
||||
<Flexbox
|
||||
@@ -43,7 +53,12 @@ const KnowledgeBase = () => {
|
||||
/>
|
||||
<div style={{ lineHeight: '14px' }}>{t('knowledgeBase.title')}</div>
|
||||
</Flexbox>
|
||||
<ActionIcon icon={PlusIcon} onClick={open} size={'small'} title={t('knowledgeBase.new')} />
|
||||
<ActionIcon
|
||||
icon={PlusIcon}
|
||||
onClick={handleCreate}
|
||||
size={'small'}
|
||||
title={t('knowledgeBase.new')}
|
||||
/>
|
||||
</Flexbox>
|
||||
|
||||
{showList && <KnowledgeBaseList />}
|
||||
@@ -0,0 +1,99 @@
|
||||
'use client';
|
||||
|
||||
import { Modal } from '@lobehub/ui';
|
||||
import { ConfigProvider } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
|
||||
import { fileManagerSelectors, useFileStore } from '@/store/file';
|
||||
|
||||
import { DETAIL_PANEL_WIDTH } from '../components/FileDetail';
|
||||
import FileDetail from '../components/modal/FileDetail';
|
||||
import FilePreview from '../components/modal/FilePreview';
|
||||
import { useFileModalId, useSetFileModalId } from './useFileQueryParam';
|
||||
|
||||
const useStyles = createStyles(({ css, token }, showDetail: boolean) => {
|
||||
return {
|
||||
body: css`
|
||||
height: 100%;
|
||||
max-height: calc(100dvh - 56px) !important;
|
||||
`,
|
||||
content: css`
|
||||
height: 100%;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
`,
|
||||
extra: css`
|
||||
position: fixed;
|
||||
z-index: ${token.zIndexPopupBase + 10};
|
||||
inset-block: 0 0;
|
||||
inset-inline-end: 0;
|
||||
|
||||
width: ${DETAIL_PANEL_WIDTH}px;
|
||||
border-inline-start: 1px solid ${token.colorSplit};
|
||||
|
||||
background: ${token.colorBgLayout};
|
||||
`,
|
||||
header: css`
|
||||
background: transparent !important;
|
||||
`,
|
||||
modal: css`
|
||||
position: relative;
|
||||
inset-block-start: 0;
|
||||
|
||||
width: ${showDetail ? `calc(100vw - ${DETAIL_PANEL_WIDTH}px) ` : '100vw'} !important;
|
||||
max-width: none;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding-block-end: 0;
|
||||
|
||||
> div {
|
||||
height: 100%;
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* FileModalQueryRoute component for Knowledge routes
|
||||
* Renders the file preview modal when a 'file' query parameter is present
|
||||
* Example: ?file=file_123
|
||||
* File data is fetched from the Zustand store by FilePreview and FileDetail components
|
||||
*/
|
||||
const FileModalQueryRoute = memo(() => {
|
||||
const fileId = useFileModalId();
|
||||
const setFileModalId = useSetFileModalId();
|
||||
const file = useFileStore(fileManagerSelectors.getFileById(fileId));
|
||||
const showDetail = useMemo(() => Boolean(fileId && file), [fileId, file]);
|
||||
const { styles } = useStyles(showDetail);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setFileModalId(undefined);
|
||||
}, [setFileModalId]);
|
||||
|
||||
if (!showDetail || !fileId) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfigProvider theme={{ token: { motion: false } }}>
|
||||
<Modal
|
||||
className={styles.modal}
|
||||
classNames={{ body: styles.body, content: styles.content, header: styles.header }}
|
||||
footer={false}
|
||||
onCancel={handleClose}
|
||||
open={showDetail}
|
||||
width={'auto'}
|
||||
>
|
||||
<FilePreview id={fileId} />
|
||||
</Modal>
|
||||
</ConfigProvider>
|
||||
<div className={styles.extra}>
|
||||
<FileDetail id={fileId} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
FileModalQueryRoute.displayName = 'FileModalQueryRoute';
|
||||
|
||||
export default FileModalQueryRoute;
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
* Query parameter key for file modal
|
||||
* Changed from 'files' to 'file' for better semantics
|
||||
*/
|
||||
export const FILE_MODAL_QUERY_KEY = 'file';
|
||||
|
||||
/**
|
||||
* Hook to get and set the file modal ID from URL query parameters
|
||||
* Uses react-router-dom's useSearchParams for MemoryRouter compatibility
|
||||
* Supports both ?file=[id] and legacy ?files=[id]
|
||||
*/
|
||||
export const useFileModalId = (): string | undefined => {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// Support both 'file' and legacy 'files' for backwards compatibility
|
||||
return searchParams.get(FILE_MODAL_QUERY_KEY) ?? searchParams.get('files') ?? undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to set the file modal ID in the URL query parameters
|
||||
* Uses ?file=[id] format for the new knowledge routes
|
||||
*/
|
||||
export const useSetFileModalId = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
return (id?: string) => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
|
||||
// Remove both new and legacy query params
|
||||
newParams.delete(FILE_MODAL_QUERY_KEY);
|
||||
newParams.delete('files');
|
||||
|
||||
if (id) {
|
||||
newParams.set(FILE_MODAL_QUERY_KEY, id);
|
||||
}
|
||||
|
||||
setSearchParams(newParams, { replace: true });
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Standalone function to set file modal ID (for use outside hooks)
|
||||
* This creates a callback that can be passed to components
|
||||
*/
|
||||
export const createSetFileModalId = (setSearchParams: ReturnType<typeof useSearchParams>[1]) => {
|
||||
return (id?: string) => {
|
||||
setSearchParams(
|
||||
(prev) => {
|
||||
const newParams = new URLSearchParams(prev);
|
||||
|
||||
// Remove both new and legacy query params
|
||||
newParams.delete(FILE_MODAL_QUERY_KEY);
|
||||
newParams.delete('files');
|
||||
|
||||
if (id) {
|
||||
newParams.set(FILE_MODAL_QUERY_KEY, id);
|
||||
}
|
||||
|
||||
return newParams;
|
||||
},
|
||||
{ replace: true },
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import FileManager from '@/features/FileManager';
|
||||
import FilePanel from '@/features/FileSidePanel';
|
||||
import { knowledgeBaseSelectors, useKnowledgeBaseStore } from '@/store/knowledgeBase';
|
||||
|
||||
import Menu from './features/Menu';
|
||||
import { useKnowledgeBaseItem } from './hooks/useKnowledgeItem';
|
||||
|
||||
const RepoClientPage = memo<{ id: string }>(({ id }) => {
|
||||
useKnowledgeBaseItem(id);
|
||||
const name = useKnowledgeBaseStore(knowledgeBaseSelectors.getKnowledgeBaseNameById(id));
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilePanel>
|
||||
<Menu id={id} />
|
||||
</FilePanel>
|
||||
<Flexbox flex={1} style={{ overflow: 'hidden', position: 'relative' }}>
|
||||
<FileManager knowledgeBaseId={id} title={name} />
|
||||
</Flexbox>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default RepoClientPage;
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { LayoutProps } from '../type';
|
||||
|
||||
const Layout = ({ children }: LayoutProps) => {
|
||||
return (
|
||||
<Flexbox
|
||||
height={'100%'}
|
||||
horizontal
|
||||
style={{ maxWidth: 'calc(100vw - 64px)', overflow: 'hidden', position: 'relative' }}
|
||||
width={'100%'}
|
||||
>
|
||||
{children}
|
||||
</Flexbox>
|
||||
);
|
||||
};
|
||||
|
||||
Layout.displayName = 'DesktopRepoLayout';
|
||||
|
||||
export default Layout;
|
||||
@@ -1,37 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { createStyles } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { useShowMobileWorkspace } from '@/hooks/useShowMobileWorkspace';
|
||||
|
||||
import { LayoutProps } from './type';
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
main: css`
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: ${token.colorBgLayout};
|
||||
`,
|
||||
}));
|
||||
|
||||
const Layout = memo<LayoutProps>(({ children }) => {
|
||||
const showMobileWorkspace = useShowMobileWorkspace();
|
||||
const { styles } = useStyles();
|
||||
|
||||
return (
|
||||
<Flexbox
|
||||
className={styles.main}
|
||||
height="100%"
|
||||
style={showMobileWorkspace ? { display: 'none' } : undefined}
|
||||
width="100%"
|
||||
>
|
||||
{children}
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
Layout.displayName = 'MobileChatLayout';
|
||||
|
||||
export default Layout;
|
||||
@@ -1,5 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export interface LayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { createStyles } from 'antd-style';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
container: css`
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background: ${token.colorBgContainer};
|
||||
`,
|
||||
}));
|
||||
|
||||
const Container = ({ children }: PropsWithChildren) => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
return (
|
||||
<Flexbox className={styles.container} height={'100%'}>
|
||||
{children}
|
||||
</Flexbox>
|
||||
);
|
||||
};
|
||||
|
||||
export default Container;
|
||||
@@ -1,35 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Tabs as TabsNav } from '@lobehub/ui';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
export const Tabs = ({ knowledgeBaseId }: { knowledgeBaseId: string }) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
const key = pathname.split('/').pop();
|
||||
|
||||
return (
|
||||
<TabsNav
|
||||
activeKey={key}
|
||||
items={[
|
||||
{
|
||||
key: 'dataset',
|
||||
label: (
|
||||
<Link href={`/repos/${knowledgeBaseId}/evals/dataset`} style={{ color: 'initial' }}>
|
||||
数据集
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'evaluation',
|
||||
label: (
|
||||
<Link href={`/repos/${knowledgeBaseId}/evals/evaluation`} style={{ color: 'initial' }}>
|
||||
评测任务
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
import { CreateNewEvalDatasets } from '@lobechat/types';
|
||||
import { Button, Form, Input } from '@lobehub/ui';
|
||||
import { memo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useKnowledgeBaseStore } from '@/store/knowledgeBase';
|
||||
|
||||
interface CreateFormProps {
|
||||
knowledgeBaseId: string;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const CreateForm = memo<CreateFormProps>(({ onClose, knowledgeBaseId }) => {
|
||||
const { t } = useTranslation('ragEval');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const createNewDataset = useKnowledgeBaseStore((s) => s.createNewDataset);
|
||||
|
||||
const onFinish = async (values: CreateNewEvalDatasets) => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await createNewDataset({ ...values, knowledgeBaseId });
|
||||
setLoading(false);
|
||||
onClose?.();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
footer={
|
||||
<Button block htmlType={'submit'} loading={loading} type={'primary'}>
|
||||
{t('addDataset.confirm')}
|
||||
</Button>
|
||||
}
|
||||
gap={16}
|
||||
items={[
|
||||
{
|
||||
children: <Input autoFocus placeholder={t('addDataset.name.placeholder')} />,
|
||||
label: t('addDataset.name.placeholder'),
|
||||
name: 'name',
|
||||
rules: [{ message: t('addDataset.name.required'), required: true }],
|
||||
},
|
||||
{
|
||||
children: <Input placeholder={t('addDataset.description.placeholder')} />,
|
||||
label: t('addDataset.description.placeholder'),
|
||||
name: 'description',
|
||||
},
|
||||
]}
|
||||
itemsType={'flat'}
|
||||
layout={'vertical'}
|
||||
onFinish={onFinish}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default CreateForm;
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Icon } from '@lobehub/ui';
|
||||
import { SheetIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { createModal } from '@/components/FunctionModal';
|
||||
|
||||
import CreateForm from './CreateForm';
|
||||
|
||||
const Title = () => {
|
||||
const { t } = useTranslation('ragEval');
|
||||
|
||||
return (
|
||||
<Flexbox gap={8} horizontal>
|
||||
<Icon icon={SheetIcon} />
|
||||
{t('addDataset.title')}
|
||||
</Flexbox>
|
||||
);
|
||||
};
|
||||
|
||||
interface CreateDatasetModalProps {
|
||||
knowledgeBaseId: string;
|
||||
}
|
||||
|
||||
export const useCreateDatasetModal = createModal<CreateDatasetModalProps>((instance, params) => ({
|
||||
content: (
|
||||
<Flexbox paddingInline={16} style={{ paddingBottom: 16 }}>
|
||||
<CreateForm
|
||||
knowledgeBaseId={params!.knowledgeBaseId}
|
||||
onClose={() => {
|
||||
instance.current?.destroy();
|
||||
}}
|
||||
/>
|
||||
</Flexbox>
|
||||
),
|
||||
title: <Title />,
|
||||
}));
|
||||
@@ -1,126 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { ProColumns, ProTable } from '@ant-design/pro-components';
|
||||
import { EvalDatasetRecordRefFile } from '@lobechat/types';
|
||||
import { ActionIcon, Button, Text } from '@lobehub/ui';
|
||||
import { Upload } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { Edit2Icon, Trash2Icon } from 'lucide-react';
|
||||
import { parseAsInteger, useQueryState } from 'nuqs';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Center, Flexbox } from 'react-layout-kit';
|
||||
|
||||
import FileIcon from '@/components/FileIcon';
|
||||
import { ragEvalService } from '@/services/ragEval';
|
||||
import { useKnowledgeBaseStore } from '@/store/knowledgeBase';
|
||||
|
||||
const createRequest = (activeDatasetId: number) => async () => {
|
||||
const records = await ragEvalService.getDatasetRecords(activeDatasetId);
|
||||
|
||||
return {
|
||||
data: records,
|
||||
success: true,
|
||||
total: records.length,
|
||||
};
|
||||
};
|
||||
|
||||
const useStyles = createStyles(({ css }) => ({
|
||||
container: css`
|
||||
padding-block: 0;
|
||||
padding-inline: 12px;
|
||||
`,
|
||||
icon: css`
|
||||
min-width: 24px;
|
||||
border-radius: 4px;
|
||||
`,
|
||||
title: css`
|
||||
font-size: 16px;
|
||||
`,
|
||||
}));
|
||||
|
||||
const DatasetDetail = () => {
|
||||
const { t } = useTranslation(['ragEval', 'common']);
|
||||
const { styles } = useStyles();
|
||||
const [importDataset] = useKnowledgeBaseStore((s) => [s.importDataset]);
|
||||
|
||||
const [activeDatasetId] = useQueryState('id', parseAsInteger);
|
||||
|
||||
const columns: ProColumns[] = [
|
||||
{
|
||||
dataIndex: 'question',
|
||||
ellipsis: true,
|
||||
title: t('dataset.list.table.columns.question.title'),
|
||||
width: '40%',
|
||||
},
|
||||
{ dataIndex: 'ideal', ellipsis: true, title: t('dataset.list.table.columns.ideal.title') },
|
||||
{
|
||||
dataIndex: 'referenceFiles',
|
||||
render: (dom, entity) => {
|
||||
const referenceFiles = entity.referenceFiles as EvalDatasetRecordRefFile[];
|
||||
|
||||
return (
|
||||
!!referenceFiles && (
|
||||
<Flexbox>
|
||||
{referenceFiles?.map((file) => (
|
||||
<Flexbox gap={4} horizontal key={file.id}>
|
||||
<FileIcon fileName={file.name} fileType={file.fileType} size={20} />
|
||||
<Text ellipsis={{ tooltip: true }}>{file.name}</Text>
|
||||
</Flexbox>
|
||||
))}
|
||||
</Flexbox>
|
||||
)
|
||||
);
|
||||
},
|
||||
title: t('dataset.list.table.columns.referenceFiles.title'),
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
dataIndex: 'actions',
|
||||
render: () => (
|
||||
<Flexbox gap={4} horizontal>
|
||||
<ActionIcon icon={Edit2Icon} size={'small'} title={t('edit', { ns: 'common' })} />
|
||||
<ActionIcon icon={Trash2Icon} size={'small'} title={t('delete', { ns: 'common' })} />
|
||||
</Flexbox>
|
||||
),
|
||||
title: t('dataset.list.table.columns.actions'),
|
||||
|
||||
width: 80,
|
||||
},
|
||||
];
|
||||
|
||||
const request = !!activeDatasetId ? createRequest(activeDatasetId) : undefined;
|
||||
|
||||
return !activeDatasetId ? (
|
||||
<Center height={'100%'} width={'100%'}>
|
||||
{t('dataset.list.table.notSelected')}
|
||||
</Center>
|
||||
) : (
|
||||
<Flexbox className={styles.container} gap={24}>
|
||||
<ProTable
|
||||
columns={columns}
|
||||
request={request}
|
||||
search={false}
|
||||
size={'small'}
|
||||
toolbar={{
|
||||
actions: [
|
||||
<Upload
|
||||
beforeUpload={async (file) => {
|
||||
await importDataset(file, activeDatasetId);
|
||||
|
||||
return false;
|
||||
}}
|
||||
key={'upload'}
|
||||
multiple={false}
|
||||
showUploadList={false}
|
||||
>
|
||||
<Button type={'primary'}>{t('dataset.list.table.actions.importData')}</Button>
|
||||
</Upload>,
|
||||
],
|
||||
title: <div className={styles.title}>{t('dataset.list.table.title')}</div>,
|
||||
}}
|
||||
/>
|
||||
</Flexbox>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatasetDetail;
|
||||
@@ -1,57 +0,0 @@
|
||||
import { RAGEvalDataSetItem } from '@lobechat/types';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { parseAsInteger, useQueryState } from 'nuqs';
|
||||
import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
active: css`
|
||||
background: ${token.colorFillTertiary};
|
||||
|
||||
&:hover {
|
||||
background-color: ${token.colorFillSecondary};
|
||||
}
|
||||
`,
|
||||
container: css`
|
||||
cursor: pointer;
|
||||
|
||||
margin-block-end: 2px;
|
||||
padding-block: 12px;
|
||||
padding-inline: 8px;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
background-color: ${token.colorFillTertiary};
|
||||
}
|
||||
`,
|
||||
icon: css`
|
||||
min-width: 24px;
|
||||
border-radius: 4px;
|
||||
`,
|
||||
title: css`
|
||||
text-align: start;
|
||||
`,
|
||||
}));
|
||||
|
||||
const Item = memo<RAGEvalDataSetItem>(({ name, description, id }) => {
|
||||
const { styles, cx } = useStyles();
|
||||
|
||||
const [activeDatasetId, activateDataset] = useQueryState('id', parseAsInteger);
|
||||
|
||||
const isActive = activeDatasetId === id;
|
||||
return (
|
||||
<Flexbox
|
||||
className={cx(styles.container, isActive && styles.active)}
|
||||
onClick={() => {
|
||||
if (!isActive) {
|
||||
activateDataset(id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={styles.title}>{name}</div>
|
||||
{description && <div>{description}</div>}
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default Item;
|
||||
@@ -1,31 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { RAGEvalDataSetItem } from '@lobechat/types';
|
||||
import { ActionIcon } from '@lobehub/ui';
|
||||
import { PlusIcon } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
|
||||
import Item from './Item';
|
||||
|
||||
interface DatasetListProps {
|
||||
dataSource: RAGEvalDataSetItem[];
|
||||
}
|
||||
|
||||
const DatasetList = memo<DatasetListProps>(({ dataSource }) => {
|
||||
const { t } = useTranslation('ragEval');
|
||||
|
||||
return (
|
||||
<Flexbox gap={24} height={'100%'}>
|
||||
<Flexbox align={'center'} horizontal justify={'space-between'}>
|
||||
<span>{t('dataset.list.title')}</span>
|
||||
<ActionIcon icon={PlusIcon} size={'small'} />
|
||||
</Flexbox>
|
||||
<Virtuoso data={dataSource} itemContent={(index, data) => <Item {...data} key={data.id} />} />
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default DatasetList;
|
||||
@@ -1,33 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Center, Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { useCreateDatasetModal } from '../CreateDataset';
|
||||
|
||||
interface EmptyGuideProps {
|
||||
knowledgeBaseId: string;
|
||||
}
|
||||
|
||||
const EmptyGuide = memo<EmptyGuideProps>(({ knowledgeBaseId }) => {
|
||||
const { t } = useTranslation('ragEval');
|
||||
const modal = useCreateDatasetModal();
|
||||
return (
|
||||
<Center gap={24} height={'100%'} width={'100%'}>
|
||||
<div>{t('dataset.emptyGuide')}</div>
|
||||
<Flexbox gap={8} horizontal>
|
||||
<Button
|
||||
onClick={() => {
|
||||
modal.open({ knowledgeBaseId });
|
||||
}}
|
||||
type={'primary'}
|
||||
>
|
||||
{t('dataset.addNewButton')}
|
||||
</Button>
|
||||
</Flexbox>
|
||||
</Center>
|
||||
);
|
||||
});
|
||||
export default EmptyGuide;
|
||||
@@ -1,52 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { createStyles } from 'antd-style';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import Loading from '@/components/Loading/BrandTextLoading';
|
||||
import { useKnowledgeBaseStore } from '@/store/knowledgeBase';
|
||||
|
||||
import DatasetDetail from './DatasetDetail';
|
||||
import DatasetList from './DatasetList';
|
||||
import EmptyGuide from './EmptyGuide';
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
sider: css`
|
||||
padding-inline-end: 12px;
|
||||
border-inline-end: 1px solid ${token.colorSplit};
|
||||
`,
|
||||
}));
|
||||
|
||||
interface Params {
|
||||
id: string;
|
||||
}
|
||||
|
||||
type Props = { params: Params & Promise<Params> };
|
||||
|
||||
const Dataset = ({ params }: Props) => {
|
||||
const { styles } = useStyles();
|
||||
const knowledgeBaseId = params.id;
|
||||
|
||||
const useFetchDatasets = useKnowledgeBaseStore((s) => s.useFetchDatasets);
|
||||
|
||||
const { data, isLoading } = useFetchDatasets(knowledgeBaseId);
|
||||
|
||||
const isEmpty = data?.length === 0;
|
||||
|
||||
return isLoading ? (
|
||||
<Loading />
|
||||
) : isEmpty ? (
|
||||
<EmptyGuide knowledgeBaseId={knowledgeBaseId} />
|
||||
) : (
|
||||
<Flexbox height={'100%'} horizontal>
|
||||
<Flexbox className={styles.sider} width={200}>
|
||||
<DatasetList dataSource={data!} />
|
||||
</Flexbox>
|
||||
<Flexbox width={'100%'}>
|
||||
<DatasetDetail />
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dataset;
|
||||
@@ -1,89 +0,0 @@
|
||||
import { CreateNewEvalEvaluation } from '@lobechat/types';
|
||||
import { Button, Form, Input, Select, TextArea } from '@lobehub/ui';
|
||||
import { memo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useKnowledgeBaseStore } from '@/store/knowledgeBase';
|
||||
|
||||
interface CreateFormProps {
|
||||
knowledgeBaseId: string;
|
||||
onClose?: () => void;
|
||||
onCreate?: () => void;
|
||||
}
|
||||
|
||||
const CreateForm = memo<CreateFormProps>(({ onClose, onCreate, knowledgeBaseId }) => {
|
||||
const { t } = useTranslation('ragEval');
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [useFetchDatasets, createNewEvaluation] = useKnowledgeBaseStore((s) => [
|
||||
s.useFetchDatasets,
|
||||
s.createNewEvaluation,
|
||||
]);
|
||||
|
||||
const { data, isLoading } = useFetchDatasets(knowledgeBaseId);
|
||||
|
||||
const onFinish = async (values: CreateNewEvalEvaluation) => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await createNewEvaluation({ ...values, knowledgeBaseId });
|
||||
setLoading(false);
|
||||
onCreate?.();
|
||||
onClose?.();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
footer={
|
||||
<Button block htmlType={'submit'} loading={loading} type={'primary'}>
|
||||
{t('evaluation.addEvaluation.confirm')}
|
||||
</Button>
|
||||
}
|
||||
gap={16}
|
||||
items={[
|
||||
{
|
||||
children: (
|
||||
<Input autoFocus placeholder={t('evaluation.addEvaluation.name.placeholder')} />
|
||||
),
|
||||
label: t('evaluation.addEvaluation.name.placeholder'),
|
||||
name: 'name',
|
||||
rules: [{ message: t('evaluation.addEvaluation.name.required'), required: true }],
|
||||
},
|
||||
{
|
||||
children: (
|
||||
<TextArea
|
||||
placeholder={t('evaluation.addEvaluation.description.placeholder')}
|
||||
style={{ minHeight: 120 }}
|
||||
/>
|
||||
),
|
||||
label: t('evaluation.addEvaluation.description.placeholder'),
|
||||
name: 'description',
|
||||
},
|
||||
{
|
||||
children: (
|
||||
<Select
|
||||
loading={isLoading}
|
||||
options={data?.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}))}
|
||||
placeholder={t('evaluation.addEvaluation.datasetId.placeholder')}
|
||||
/>
|
||||
),
|
||||
label: t('evaluation.addEvaluation.datasetId.placeholder'),
|
||||
name: 'datasetId',
|
||||
rules: [{ message: t('evaluation.addEvaluation.datasetId.required'), required: true }],
|
||||
},
|
||||
]}
|
||||
itemsType={'flat'}
|
||||
layout={'vertical'}
|
||||
onFinish={onFinish}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default CreateForm;
|
||||
@@ -1,28 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useCreateDatasetModal } from './useModal';
|
||||
|
||||
interface CreateEvaluationProps {
|
||||
knowledgeBaseId: string;
|
||||
onCreate?: () => void;
|
||||
}
|
||||
|
||||
const CreateEvaluation = memo<CreateEvaluationProps>(({ knowledgeBaseId, onCreate }) => {
|
||||
const { t } = useTranslation('ragEval');
|
||||
const modal = useCreateDatasetModal();
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
modal.open({ knowledgeBaseId, onCreate });
|
||||
}}
|
||||
type={'primary'}
|
||||
>
|
||||
{t('evaluation.addNewButton')}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
export default CreateEvaluation;
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Icon } from '@lobehub/ui';
|
||||
import { SheetIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { createModal } from '@/components/FunctionModal';
|
||||
|
||||
import CreateForm from './CreateForm';
|
||||
|
||||
const Title = () => {
|
||||
const { t } = useTranslation('ragEval');
|
||||
|
||||
return (
|
||||
<Flexbox gap={8} horizontal>
|
||||
<Icon icon={SheetIcon} />
|
||||
{t('addDataset.title')}
|
||||
</Flexbox>
|
||||
);
|
||||
};
|
||||
|
||||
interface CreateDatasetModalProps {
|
||||
knowledgeBaseId: string;
|
||||
onCreate?: () => void;
|
||||
}
|
||||
|
||||
export const useCreateDatasetModal = createModal<CreateDatasetModalProps>((instance, params) => ({
|
||||
content: (
|
||||
<Flexbox paddingInline={16} style={{ paddingBottom: 24 }}>
|
||||
<CreateForm
|
||||
knowledgeBaseId={params!.knowledgeBaseId}
|
||||
onClose={() => {
|
||||
instance.current?.destroy();
|
||||
}}
|
||||
onCreate={params?.onCreate}
|
||||
/>
|
||||
</Flexbox>
|
||||
),
|
||||
title: <Title />,
|
||||
}));
|
||||
@@ -1,25 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Center, Flexbox } from 'react-layout-kit';
|
||||
|
||||
import CreateEvaluationButton from '../CreateEvaluation';
|
||||
|
||||
interface EmptyGuideProps {
|
||||
knowledgeBaseId: string;
|
||||
}
|
||||
|
||||
const EmptyGuide = memo<EmptyGuideProps>(({ knowledgeBaseId }) => {
|
||||
const { t } = useTranslation('ragEval');
|
||||
|
||||
return (
|
||||
<Center gap={24} height={'100%'} width={'100%'}>
|
||||
<div>{t('evaluation.emptyGuide')}</div>
|
||||
<Flexbox gap={8} horizontal>
|
||||
<CreateEvaluationButton knowledgeBaseId={knowledgeBaseId} />
|
||||
</Flexbox>
|
||||
</Center>
|
||||
);
|
||||
});
|
||||
export default EmptyGuide;
|
||||
@@ -1,210 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
|
||||
import { EvalEvaluationStatus, RAGEvalEvaluationItem } from '@lobechat/types';
|
||||
import { ActionIcon, Button, ButtonProps, Icon } from '@lobehub/ui';
|
||||
import { App } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { DownloadIcon, PlayIcon, RotateCcwIcon, Trash2Icon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { ragEvalService } from '@/services/ragEval';
|
||||
import { useKnowledgeBaseStore } from '@/store/knowledgeBase';
|
||||
|
||||
import CreateEvaluationButton from '../CreateEvaluation';
|
||||
|
||||
const createRequest = (knowledgeBaseId: string) => async () => {
|
||||
const records = await ragEvalService.getEvaluationList(knowledgeBaseId);
|
||||
|
||||
return {
|
||||
data: records,
|
||||
success: true,
|
||||
total: records.length,
|
||||
};
|
||||
};
|
||||
|
||||
const useStyles = createStyles(({ css }) => ({
|
||||
icon: css`
|
||||
min-width: 24px;
|
||||
border-radius: 4px;
|
||||
`,
|
||||
title: css`
|
||||
font-size: 16px;
|
||||
`,
|
||||
}));
|
||||
|
||||
const EvaluationList = ({ knowledgeBaseId }: { knowledgeBaseId: string }) => {
|
||||
const { t } = useTranslation(['ragEval', 'common']);
|
||||
const { styles } = useStyles();
|
||||
const [removeEvaluation, runEvaluation, checkEvaluationStatus] = useKnowledgeBaseStore((s) => [
|
||||
s.removeEvaluation,
|
||||
s.runEvaluation,
|
||||
s.checkEvaluationStatus,
|
||||
]);
|
||||
const [isCheckingStatus, setCheckingStatus] = useState(false);
|
||||
const { modal } = App.useApp();
|
||||
const actionRef = useRef<ActionType>(null);
|
||||
|
||||
const columns: ProColumns<RAGEvalEvaluationItem>[] = [
|
||||
{
|
||||
dataIndex: 'name',
|
||||
ellipsis: true,
|
||||
title: t('evaluation.table.columns.name.title'),
|
||||
},
|
||||
{
|
||||
dataIndex: ['dataset', 'id'],
|
||||
render: (dom, entity) => {
|
||||
return (
|
||||
<Link
|
||||
href={`/repos/${knowledgeBaseId}/evals/dataset?id=${entity.dataset.id}`}
|
||||
style={{ color: 'initial' }}
|
||||
target={'_blank'}
|
||||
>
|
||||
{entity.dataset.name}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
title: t('evaluation.table.columns.datasetId.title'),
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
dataIndex: 'status',
|
||||
title: t('evaluation.table.columns.status.title'),
|
||||
valueEnum: {
|
||||
[EvalEvaluationStatus.Error]: {
|
||||
status: 'error',
|
||||
text: t('evaluation.table.columns.status.error'),
|
||||
},
|
||||
[EvalEvaluationStatus.Processing]: {
|
||||
status: 'processing',
|
||||
text: t('evaluation.table.columns.status.processing'),
|
||||
},
|
||||
[EvalEvaluationStatus.Pending]: {
|
||||
status: 'default',
|
||||
text: t('evaluation.table.columns.status.pending'),
|
||||
},
|
||||
[EvalEvaluationStatus.Success]: {
|
||||
status: 'success',
|
||||
text: t('evaluation.table.columns.status.success'),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: ['recordsStats', 'total'],
|
||||
render: (dom, entity) => {
|
||||
return entity.status === 'Pending'
|
||||
? entity.recordsStats.total
|
||||
: `${entity.recordsStats.success}/${entity.recordsStats.total}`;
|
||||
},
|
||||
title: t('evaluation.table.columns.records.title'),
|
||||
},
|
||||
{
|
||||
dataIndex: 'actions',
|
||||
render: (_, entity) => {
|
||||
const actionsMap: Record<EvalEvaluationStatus, ButtonProps> = {
|
||||
[EvalEvaluationStatus.Pending]: {
|
||||
children: t('evaluation.table.columns.actions.run'),
|
||||
icon: <Icon icon={PlayIcon} />,
|
||||
onClick: () => {
|
||||
modal.confirm({
|
||||
content: t('evaluation.table.columns.actions.confirmRun'),
|
||||
onOk: async () => {
|
||||
await runEvaluation(entity.id);
|
||||
await actionRef.current?.reload();
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
[EvalEvaluationStatus.Error]: {
|
||||
children: t('evaluation.table.columns.actions.retry'),
|
||||
icon: <Icon icon={RotateCcwIcon} />,
|
||||
onClick: () => {
|
||||
modal.confirm({
|
||||
content: t('evaluation.table.columns.actions.confirmRun'),
|
||||
onOk: async () => {
|
||||
await runEvaluation(entity.id);
|
||||
await actionRef.current?.reload();
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
[EvalEvaluationStatus.Processing]: {
|
||||
children: t('evaluation.table.columns.actions.checkStatus'),
|
||||
icon: null,
|
||||
loading: isCheckingStatus,
|
||||
onClick: async () => {
|
||||
setCheckingStatus(true);
|
||||
await checkEvaluationStatus(entity.id);
|
||||
setCheckingStatus(false);
|
||||
await actionRef.current?.reload();
|
||||
},
|
||||
},
|
||||
[EvalEvaluationStatus.Success]: {
|
||||
children: t('evaluation.table.columns.actions.downloadRecords'),
|
||||
icon: <Icon icon={DownloadIcon} />,
|
||||
onClick: async () => {
|
||||
window.open(entity.evalRecordsUrl);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const actionProps = actionsMap[entity.status];
|
||||
|
||||
return (
|
||||
<Flexbox gap={4} horizontal>
|
||||
{!actionProps ? null : <Button {...actionProps} size={'small'} />}
|
||||
<ActionIcon
|
||||
icon={Trash2Icon}
|
||||
onClick={async () => {
|
||||
modal.confirm({
|
||||
content: t('evaluation.table.columns.actions.confirmDelete'),
|
||||
okButtonProps: {
|
||||
danger: true,
|
||||
},
|
||||
onOk: async () => {
|
||||
await removeEvaluation(entity.id);
|
||||
await actionRef.current?.reload();
|
||||
},
|
||||
});
|
||||
}}
|
||||
size={'small'}
|
||||
title={t('delete', { ns: 'common' })}
|
||||
/>
|
||||
</Flexbox>
|
||||
);
|
||||
},
|
||||
title: t('evaluation.table.columns.actions.title'),
|
||||
width: 120,
|
||||
},
|
||||
];
|
||||
|
||||
const request = knowledgeBaseId ? createRequest(knowledgeBaseId) : undefined;
|
||||
|
||||
return (
|
||||
<Flexbox gap={24}>
|
||||
<ProTable
|
||||
actionRef={actionRef}
|
||||
columns={columns}
|
||||
request={request}
|
||||
search={false}
|
||||
toolbar={{
|
||||
actions: [
|
||||
<CreateEvaluationButton
|
||||
key={'new'}
|
||||
knowledgeBaseId={knowledgeBaseId}
|
||||
onCreate={() => {
|
||||
actionRef.current?.reload();
|
||||
}}
|
||||
/>,
|
||||
],
|
||||
title: <div className={styles.title}>{t('evaluation.table.title')}</div>,
|
||||
}}
|
||||
/>
|
||||
</Flexbox>
|
||||
);
|
||||
};
|
||||
|
||||
export default EvaluationList;
|
||||
@@ -1,37 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import Loading from '@/components/Loading/BrandTextLoading';
|
||||
import { useKnowledgeBaseStore } from '@/store/knowledgeBase';
|
||||
|
||||
import EmptyGuide from './EmptyGuide';
|
||||
import EvaluationList from './EvaluationList';
|
||||
|
||||
interface Params {
|
||||
id: string;
|
||||
}
|
||||
|
||||
type Props = { params: Params & Promise<Params> };
|
||||
|
||||
const Evaluation = ({ params }: Props) => {
|
||||
const knowledgeBaseId = params.id;
|
||||
|
||||
const useFetchEvaluation = useKnowledgeBaseStore((s) => s.useFetchEvaluationList);
|
||||
|
||||
const { data, isLoading } = useFetchEvaluation(knowledgeBaseId);
|
||||
|
||||
const isEmpty = data?.length === 0;
|
||||
|
||||
return isLoading ? (
|
||||
<Loading />
|
||||
) : isEmpty ? (
|
||||
<EmptyGuide knowledgeBaseId={knowledgeBaseId} />
|
||||
) : (
|
||||
<Flexbox height={'100%'}>
|
||||
<EvaluationList knowledgeBaseId={knowledgeBaseId} />
|
||||
</Flexbox>
|
||||
);
|
||||
};
|
||||
|
||||
export default Evaluation;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user