Compare commits

...

12 Commits

Author SHA1 Message Date
Junyan Qin
3204292360 chore: bump version to 4.8.2 and update langbot-plugin and pyseekdb versions in uv.lock 2026-01-31 12:54:05 +08:00
Junyan Qin
e0d72969e3 chore(deps): update langbot-plugin version to 0.2.5 in pyproject.toml 2026-01-30 17:31:21 +08:00
Junyan Qin
a65b7ad413 chore(deps): update pyseekdb version to 1.0.0b7 in pyproject.toml 2026-01-30 13:39:36 +08:00
Junyan Qin
45df44e01b chore: update uv.lock 2026-01-30 12:42:21 +08:00
Junyan Qin
d8addb105a chore: update .gitignore and add uv.lock for dependency management 2026-01-30 12:32:39 +08:00
Junyan Qin
f17ccad665 chore: update TypeScript configuration for improved compatibility and structure 2026-01-30 12:15:19 +08:00
Junyan Qin
120ceb0b55 chore: update linting configuration to use eslint directly 2026-01-30 12:03:43 +08:00
dependabot[bot]
8a6f80a181 chore(deps): bump lodash from 4.17.21 to 4.17.23 in /web (#1944)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-30 11:25:16 +08:00
dependabot[bot]
b19e468668 chore(deps): bump next from 15.5.9 to 16.1.5 in /web (#1943)
Bumps [next](https://github.com/vercel/next.js) from 15.5.9 to 16.1.5.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.5.9...v16.1.5)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.1.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-30 11:20:08 +08:00
Junyan Qin
aeac79e1b3 feat: add tag filtering functionality to Plugin Market
- Introduced TagsFilter component for selecting and filtering plugins by tags.
- Updated PluginMarketComponent to handle tag selection and display.
- Enhanced PluginMarketCardComponent to show selected tags.
- Modified CloudServiceClient to fetch available tags from the API.
- Updated localization files to support new tag-related strings.
2026-01-29 16:08:05 +08:00
Junyan Qin
b89a240250 feat: implement LoadingSpinner component and replace existing loaders across the application 2026-01-29 15:24:23 +08:00
Junyan Qin
13f42857f5 perf: detailed control of models service displaying 2026-01-27 22:44:58 +08:00
28 changed files with 6343 additions and 176 deletions

1
.gitignore vendored
View File

@@ -42,7 +42,6 @@ botpy.log*
test.py
/web_ui
.venv/
uv.lock
/test
plugins.bak
coverage.xml

View File

@@ -1,6 +1,6 @@
[project]
name = "langbot"
version = "4.8.1"
version = "4.8.2"
description = "Production-grade platform for building agentic IM bots"
readme = "README.md"
license-files = ["LICENSE"]
@@ -63,8 +63,8 @@ dependencies = [
"langchain-text-splitters>=0.0.1",
"chromadb>=0.4.24",
"qdrant-client (>=1.15.1,<2.0.0)",
"pyseekdb>=0.1.0",
"langbot-plugin==0.2.4",
"pyseekdb==1.0.0b7",
"langbot-plugin==0.2.5",
"asyncpg>=0.30.0",
"line-bot-sdk>=3.19.0",
"tboxsdk>=0.0.10",

View File

@@ -1,3 +1,3 @@
"""LangBot - Production-grade platform for building agentic IM bots"""
__version__ = '4.8.1'
__version__ = '4.8.2'

5791
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
{
"*.{js,jsx,ts,tsx}": ["next lint --fix --file", "next lint --file"],
"*.{js,jsx,ts,tsx}": ["eslint --fix"],
"**/*": ["bash -c 'cd \"$(pwd)\" && next build"]
}

View File

@@ -6,7 +6,8 @@
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"lint-staged": "lint-staged"
},
"lint-staged": {
@@ -50,9 +51,9 @@
"i18next": "^25.1.2",
"i18next-browser-languagedetector": "^8.1.0",
"input-otp": "^1.4.2",
"lodash": "^4.17.21",
"lodash": "^4.17.23",
"lucide-react": "^0.507.0",
"next": "~15.5.9",
"next": "~16.1.5",
"next-themes": "^0.4.6",
"postcss": "^8.5.3",
"qrcode": "^1.5.4",

100
web/pnpm-lock.yaml generated
View File

@@ -99,14 +99,14 @@ dependencies:
specifier: ^1.4.2
version: 1.4.2(react-dom@19.2.1)(react@19.2.1)
lodash:
specifier: ^4.17.21
version: 4.17.21
specifier: ^4.17.23
version: 4.17.23
lucide-react:
specifier: ^0.507.0
version: 0.507.0(react@19.2.1)
next:
specifier: ~15.5.9
version: 15.5.9(react-dom@19.2.1)(react@19.2.1)
specifier: ~16.1.5
version: 16.1.5(react-dom@19.2.1)(react@19.2.1)
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.1)(react@19.2.1)
@@ -297,8 +297,8 @@ packages:
tslib: 2.8.1
dev: false
/@emnapi/core@1.7.1:
resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==}
/@emnapi/core@1.8.1:
resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
requiresBuild: true
dependencies:
'@emnapi/wasi-threads': 1.1.0
@@ -306,8 +306,8 @@ packages:
dev: true
optional: true
/@emnapi/runtime@1.7.1:
resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==}
/@emnapi/runtime@1.8.1:
resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
requiresBuild: true
dependencies:
tslib: 2.8.1
@@ -659,7 +659,7 @@ packages:
cpu: [wasm32]
requiresBuild: true
dependencies:
'@emnapi/runtime': 1.7.1
'@emnapi/runtime': 1.8.1
dev: false
optional: true
@@ -724,14 +724,14 @@ packages:
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
requiresBuild: true
dependencies:
'@emnapi/core': 1.7.1
'@emnapi/runtime': 1.7.1
'@emnapi/core': 1.8.1
'@emnapi/runtime': 1.8.1
'@tybys/wasm-util': 0.10.1
dev: true
optional: true
/@next/env@15.5.9:
resolution: {integrity: sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==}
/@next/env@16.1.5:
resolution: {integrity: sha512-CRSCPJiSZoi4Pn69RYBDI9R7YK2g59vLexPQFXY0eyw+ILevIenCywzg+DqmlBik9zszEnw2HLFOUlLAcJbL7g==}
dev: false
/@next/eslint-plugin-next@15.2.4:
@@ -740,8 +740,8 @@ packages:
fast-glob: 3.3.1
dev: true
/@next/swc-darwin-arm64@15.5.7:
resolution: {integrity: sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==}
/@next/swc-darwin-arm64@16.1.5:
resolution: {integrity: sha512-eK7Wdm3Hjy/SCL7TevlH0C9chrpeOYWx2iR7guJDaz4zEQKWcS1IMVfMb9UKBFMg1XgzcPTYPIp1Vcpukkjg6Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
@@ -749,8 +749,8 @@ packages:
dev: false
optional: true
/@next/swc-darwin-x64@15.5.7:
resolution: {integrity: sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==}
/@next/swc-darwin-x64@16.1.5:
resolution: {integrity: sha512-foQscSHD1dCuxBmGkbIr6ScAUF6pRoDZP6czajyvmXPAOFNnQUJu2Os1SGELODjKp/ULa4fulnBWoHV3XdPLfA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
@@ -758,8 +758,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-arm64-gnu@15.5.7:
resolution: {integrity: sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==}
/@next/swc-linux-arm64-gnu@16.1.5:
resolution: {integrity: sha512-qNIb42o3C02ccIeSeKjacF3HXotGsxh/FMk/rSRmCzOVMtoWH88odn2uZqF8RLsSUWHcAqTgYmPD3pZ03L9ZAA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -767,8 +767,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-arm64-musl@15.5.7:
resolution: {integrity: sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==}
/@next/swc-linux-arm64-musl@16.1.5:
resolution: {integrity: sha512-U+kBxGUY1xMAzDTXmuVMfhaWUZQAwzRaHJ/I6ihtR5SbTVUEaDRiEU9YMjy1obBWpdOBuk1bcm+tsmifYSygfw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -776,8 +776,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-x64-gnu@15.5.7:
resolution: {integrity: sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==}
/@next/swc-linux-x64-gnu@16.1.5:
resolution: {integrity: sha512-gq2UtoCpN7Ke/7tKaU7i/1L7eFLfhMbXjNghSv0MVGF1dmuoaPeEVDvkDuO/9LVa44h5gqpWeJ4mRRznjDv7LA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -785,8 +785,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-x64-musl@15.5.7:
resolution: {integrity: sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==}
/@next/swc-linux-x64-musl@16.1.5:
resolution: {integrity: sha512-bQWSE729PbXT6mMklWLf8dotislPle2L70E9q6iwETYEOt092GDn0c+TTNj26AjmeceSsC4ndyGsK5nKqHYXjQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -794,8 +794,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-arm64-msvc@15.5.7:
resolution: {integrity: sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==}
/@next/swc-win32-arm64-msvc@16.1.5:
resolution: {integrity: sha512-LZli0anutkIllMtTAWZlDqdfvjWX/ch8AFK5WgkNTvaqwlouiD1oHM+WW8RXMiL0+vAkAJyAGEzPPjO+hnrSNQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
@@ -803,8 +803,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-x64-msvc@15.5.7:
resolution: {integrity: sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==}
/@next/swc-win32-x64-msvc@16.1.5:
resolution: {integrity: sha512-7is37HJTNQGhjPpQbkKjKEboHYQnCgpVt/4rBrrln0D9nderNxZ8ZWs8w1fAtzUx7wEyYjQ+/13myFgFj6K2Ng==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -2632,6 +2632,11 @@ packages:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
dev: true
/baseline-browser-mapping@2.9.19:
resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==}
hasBin: true
dev: false
/brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
dependencies:
@@ -2687,8 +2692,8 @@ packages:
engines: {node: '>=6'}
dev: false
/caniuse-lite@1.0.30001760:
resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==}
/caniuse-lite@1.0.30001766:
resolution: {integrity: sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==}
dev: false
/ccount@2.0.1:
@@ -4535,8 +4540,8 @@ packages:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
dev: true
/lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
/lodash@4.17.23:
resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
dev: false
/log-update@6.1.0:
@@ -5112,9 +5117,9 @@ packages:
react-dom: 19.2.1(react@19.2.1)
dev: false
/next@15.5.9(react-dom@19.2.1)(react@19.2.1):
resolution: {integrity: sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
/next@16.1.5(react-dom@19.2.1)(react@19.2.1):
resolution: {integrity: sha512-f+wE+NSbiQgh3DSAlTaw2FwY5yGdVViAtp8TotNQj4kk4Q8Bh1sC/aL9aH+Rg1YAVn18OYXsRDT7U/079jgP7w==}
engines: {node: '>=20.9.0'}
hasBin: true
peerDependencies:
'@opentelemetry/api': ^1.1.0
@@ -5133,22 +5138,23 @@ packages:
sass:
optional: true
dependencies:
'@next/env': 15.5.9
'@next/env': 16.1.5
'@swc/helpers': 0.5.15
caniuse-lite: 1.0.30001760
baseline-browser-mapping: 2.9.19
caniuse-lite: 1.0.30001766
postcss: 8.4.31
react: 19.2.1
react-dom: 19.2.1(react@19.2.1)
styled-jsx: 5.1.6(react@19.2.1)
optionalDependencies:
'@next/swc-darwin-arm64': 15.5.7
'@next/swc-darwin-x64': 15.5.7
'@next/swc-linux-arm64-gnu': 15.5.7
'@next/swc-linux-arm64-musl': 15.5.7
'@next/swc-linux-x64-gnu': 15.5.7
'@next/swc-linux-x64-musl': 15.5.7
'@next/swc-win32-arm64-msvc': 15.5.7
'@next/swc-win32-x64-msvc': 15.5.7
'@next/swc-darwin-arm64': 16.1.5
'@next/swc-darwin-x64': 16.1.5
'@next/swc-linux-arm64-gnu': 16.1.5
'@next/swc-linux-arm64-musl': 16.1.5
'@next/swc-linux-x64-gnu': 16.1.5
'@next/swc-linux-x64-musl': 16.1.5
'@next/swc-win32-arm64-msvc': 16.1.5
'@next/swc-win32-x64-msvc': 16.1.5
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
@@ -5642,7 +5648,7 @@ packages:
dependencies:
clsx: 2.1.1
eventemitter3: 4.0.7
lodash: 4.17.21
lodash: 4.17.23
react: 19.2.1
react-dom: 19.2.1(react@19.2.1)
react-is: 18.3.1

View File

@@ -19,6 +19,7 @@ import {
CardDescription,
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { LoadingSpinner } from '@/components/ui/loading-spinner';
import langbotIcon from '@/app/assets/langbot-logo.webp';
function SpaceOAuthCallbackContent() {
@@ -174,9 +175,7 @@ function SpaceOAuthCallbackContent() {
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col items-center space-y-4">
{status === 'loading' && (
<Loader2 className="h-12 w-12 animate-spin text-primary" />
)}
{status === 'loading' && <LoadingSpinner size="lg" text="" />}
{status === 'confirm' && (
<>
<AlertTriangle className="h-12 w-12 text-yellow-500" />
@@ -232,7 +231,7 @@ function LoadingFallback() {
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-neutral-900">
<Card className="w-[400px] shadow-lg dark:shadow-white/10">
<CardContent className="flex flex-col items-center py-12">
<Loader2 className="h-12 w-12 animate-spin text-primary" />
<LoadingSpinner size="lg" text="" />
</CardContent>
</Card>
</div>

View File

@@ -17,7 +17,7 @@ import { Switch } from '@/components/ui/switch';
import { ControllerRenderProps } from 'react-hook-form';
import { Button } from '@/components/ui/button';
import { useEffect, useState } from 'react';
import { httpClient, systemInfo } from '@/app/infra/http/HttpClient';
import { httpClient, systemInfo, userInfo } from '@/app/infra/http';
import {
LLMModel,
Bot,
@@ -99,8 +99,11 @@ export default function DynamicFormItemComponent({
.getProviderLLMModels()
.then((resp) => {
let models = resp.models;
// Filter out space-chat-completions models when models service is disabled
if (systemInfo.disable_models_service) {
// Filter out space-chat-completions models when not logged in with space account or when models service is disabled
if (
systemInfo.disable_models_service ||
userInfo?.account_type !== 'space'
) {
models = models.filter(
(m) => m.provider?.requester !== 'space-chat-completions',
);

View File

@@ -1,7 +1,7 @@
'use client';
import styles from './HomeSidebar.module.css';
import { useEffect, useState, Suspense } from 'react';
import { useEffect, useState } from 'react';
import {
SidebarChild,
SidebarChildVO,
@@ -20,7 +20,6 @@ import {
Lightbulb,
LogOut,
KeyRound,
Loader2,
} from 'lucide-react';
import { useTheme } from 'next-themes';
@@ -59,7 +58,7 @@ function compareVersions(v1: string, v2: string): boolean {
}
// TODO 侧边导航栏要加动画
function HomeSidebarContent({
export default function HomeSidebar({
onSelectedChangeAction,
}: {
onSelectedChangeAction: (sidebarChild: SidebarChildVO) => void;
@@ -484,25 +483,3 @@ function HomeSidebarContent({
</div>
);
}
function SidebarLoadingFallback() {
return (
<div className={`${styles.sidebarContainer}`}>
<div className="flex items-center justify-center h-full">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
</div>
);
}
export default function HomeSidebar({
onSelectedChangeAction,
}: {
onSelectedChangeAction: (sidebarChild: SidebarChildVO) => void;
}) {
return (
<Suspense fallback={<SidebarLoadingFallback />}>
<HomeSidebarContent onSelectedChangeAction={onSelectedChangeAction} />
</Suspense>
);
}

View File

@@ -16,6 +16,7 @@ import { useTranslation } from 'react-i18next';
import { LLMModel, EmbeddingModel } from '@/app/infra/entities/api';
import { ExtraArg, ModelType, TestResult } from '../types';
import ExtraArgsEditor from './ExtraArgsEditor';
import { userInfo } from '@/app/infra/http';
interface ModelItemProps {
model: LLMModel | EmbeddingModel;
@@ -113,10 +114,15 @@ export default function ModelItem({
}
};
// Check if popover should be disabled (space models when not logged in)
const isPopoverDisabled =
isLangBotModels && userInfo?.account_type !== 'space';
return (
<Popover
open={isEditOpen}
open={isEditOpen && !isPopoverDisabled}
onOpenChange={(open) => {
if (isPopoverDisabled) return;
if (open) {
onOpenEditModel(model.uuid);
} else {
@@ -125,7 +131,13 @@ export default function ModelItem({
}}
>
<PopoverTrigger asChild>
<div className="flex items-center justify-between py-2 px-3 rounded-md border bg-background hover:bg-accent cursor-pointer">
<div
className={`flex items-center justify-between py-2 px-3 rounded-md border bg-background ${
isPopoverDisabled
? 'cursor-not-allowed opacity-60'
: 'hover:bg-accent cursor-pointer'
}`}
>
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium">{model.name}</span>
<Badge variant="secondary" className="text-xs">

View File

@@ -14,7 +14,7 @@ import {
FormMessage,
FormDescription,
} from '@/components/ui/form';
import { httpClient, systemInfo } from '@/app/infra/http/HttpClient';
import { httpClient, systemInfo, userInfo } from '@/app/infra/http';
import {
Select,
SelectContent,
@@ -101,8 +101,11 @@ export default function KBForm({
const getEmbeddingModelNameList = async () => {
const resp = await httpClient.getProviderEmbeddingModels();
let models = resp.models;
// Filter out space-chat-completions models when models service is disabled
if (systemInfo.disable_models_service) {
// Filter out space-chat-completions models when not logged in with space account or when models service is disabled
if (
systemInfo.disable_models_service ||
userInfo?.account_type !== 'space'
) {
models = models.filter(
(m) => m.provider?.requester !== 'space-chat-completions',
);

View File

@@ -3,9 +3,16 @@
import styles from './layout.module.css';
import HomeSidebar from '@/app/home/components/home-sidebar/HomeSidebar';
import HomeTitleBar from '@/app/home/components/home-titlebar/HomeTitleBar';
import React, { useState, useCallback, useMemo } from 'react';
import React, {
useState,
useCallback,
useMemo,
useEffect,
Suspense,
} from 'react';
import { SidebarChildVO } from '@/app/home/components/home-sidebar/HomeSidebarChild';
import { I18nObject } from '@/app/infra/entities/common';
import { userInfo, initializeUserInfo } from '@/app/infra/http';
export default function HomeLayout({
children,
@@ -19,6 +26,13 @@ export default function HomeLayout({
zh_Hans: '',
});
// Initialize user info if not already initialized
useEffect(() => {
if (!userInfo) {
initializeUserInfo();
}
}, []);
const onSelectedChangeAction = useCallback((child: SidebarChildVO) => {
setTitle(child.name);
setSubtitle(child.description);
@@ -31,7 +45,9 @@ export default function HomeLayout({
return (
<div className={styles.homeLayoutContainer}>
<aside className={styles.sidebar}>
<HomeSidebar onSelectedChangeAction={onSelectedChangeAction} />
<Suspense fallback={<div />}>
<HomeSidebar onSelectedChangeAction={onSelectedChangeAction} />
</Suspense>
</aside>
<div className={styles.main}>

View File

@@ -0,0 +1,5 @@
import { LoadingPage } from '@/components/ui/loading-spinner';
export default function Loading() {
return <LoadingPage />;
}

View File

@@ -13,6 +13,7 @@ import { MessageDetailsCard } from './components/MessageDetailsCard';
import { MessageContentRenderer } from './components/MessageContentRenderer';
import { MessageDetails } from './types/monitoring';
import { httpClient } from '@/app/infra/http/HttpClient';
import { LoadingSpinner, LoadingPage } from '@/components/ui/loading-spinner';
interface RawMessageData {
id: string;
@@ -262,11 +263,10 @@ function MonitoringPageContent() {
<TabsContent value="messages" className="p-6 m-0">
<div>
{loading && (
<div className="text-center text-gray-500 dark:text-gray-400 py-12">
<div className="inline-block animate-spin rounded-full h-10 w-10 border-b-2 border-blue-600 dark:border-blue-400 mb-4"></div>
<p className="text-sm font-medium">
{t('monitoring.messageList.loading')}
</p>
<div className="py-12 flex justify-center">
<LoadingSpinner
text={t('monitoring.messageList.loading')}
/>
</div>
)}
@@ -363,8 +363,8 @@ function MonitoringPageContent() {
{expandedMessageId === msg.id && (
<div className="border-t border-gray-200 dark:border-gray-700 p-4 bg-gray-50 dark:bg-gray-900">
{loadingDetails[msg.id] && (
<div className="text-center text-gray-500 dark:text-gray-400 py-4">
<div className="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-gray-900 dark:border-white"></div>
<div className="py-4 flex justify-center">
<LoadingSpinner size="sm" text="" />
</div>
)}
{!loadingDetails[msg.id] &&
@@ -410,9 +410,8 @@ function MonitoringPageContent() {
<TabsContent value="modelCalls" className="p-6 m-0">
<div>
{loading && (
<div className="text-center text-gray-500 dark:text-gray-400 py-12">
<div className="inline-block animate-spin rounded-full h-10 w-10 border-b-2 border-blue-600 dark:border-blue-400 mb-4"></div>
<p className="text-sm font-medium">{t('common.loading')}</p>
<div className="py-12 flex justify-center">
<LoadingSpinner text={t('common.loading')} />
</div>
)}
@@ -629,9 +628,8 @@ function MonitoringPageContent() {
<TabsContent value="errors" className="p-6 m-0">
<div>
{loading && (
<div className="text-center text-gray-500 dark:text-gray-400 py-12">
<div className="inline-block animate-spin rounded-full h-10 w-10 border-b-2 border-blue-600 dark:border-blue-400 mb-4"></div>
<p className="text-sm font-medium">{t('common.loading')}</p>
<div className="py-12 flex justify-center">
<LoadingSpinner text={t('common.loading')} />
</div>
)}
@@ -810,7 +808,7 @@ function MonitoringPageContent() {
export default function MonitoringPage() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Suspense fallback={<LoadingPage />}>
<MonitoringPageContent />
</Suspense>
);

View File

@@ -1,7 +1,6 @@
'use client';
import { useState, useEffect, useCallback, useRef, Suspense } from 'react';
import { useSearchParams } from 'next/navigation';
import { Input } from '@/components/ui/input';
import {
Select,
@@ -11,14 +10,7 @@ import {
SelectValue,
} from '@/components/ui/select';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import {
Search,
Loader2,
Wrench,
AudioWaveform,
Hash,
Book,
} from 'lucide-react';
import { Search, Wrench, AudioWaveform, Hash, Book } from 'lucide-react';
import PluginMarketCardComponent from './plugin-market-card/PluginMarketCardComponent';
import { PluginMarketCardVO } from './plugin-market-card/PluginMarketCardVO';
import { getCloudServiceClientSync } from '@/app/infra/http';
@@ -27,6 +19,9 @@ import { PluginV4 } from '@/app/infra/entities/plugin';
import { extractI18nObject } from '@/i18n/I18nProvider';
import { toast } from 'sonner';
import { ApiRespMarketplacePlugins } from '@/app/infra/entities/api';
import { LoadingSpinner } from '@/components/ui/loading-spinner';
import { TagsFilter } from './TagsFilter';
import { PluginTag } from '@/app/infra/http/CloudServiceClient';
interface SortOption {
value: string;
@@ -42,10 +37,12 @@ function MarketPageContent({
installPlugin: (plugin: PluginV4) => void;
}) {
const { t } = useTranslation();
const searchParams = useSearchParams();
const [searchQuery, setSearchQuery] = useState('');
const [componentFilter, setComponentFilter] = useState<string>('all');
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [availableTags, setAvailableTags] = useState<PluginTag[]>([]);
const [tagNames, setTagNames] = useState<Record<string, string>>({});
const [plugins, setPlugins] = useState<PluginMarketCardVO[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isLoadingMore, setIsLoadingMore] = useState(false);
@@ -111,6 +108,7 @@ function MarketPageContent({
githubURL: plugin.repository,
version: plugin.latest_version,
components: plugin.components,
tags: plugin.tags || [],
});
}, []);
@@ -128,7 +126,7 @@ function MarketPageContent({
const filterValue =
componentFilter === 'all' ? undefined : componentFilter;
// Always use searchMarketplacePlugins to support component filtering
// Always use searchMarketplacePlugins to support component filtering and tags filtering
const response =
await getCloudServiceClientSync().searchMarketplacePlugins(
isSearch && searchQuery.trim() ? searchQuery.trim() : '',
@@ -137,6 +135,7 @@ function MarketPageContent({
sortBy,
sortOrder,
filterValue,
selectedTags.length > 0 ? selectedTags : undefined,
);
const data: ApiRespMarketplacePlugins = response;
@@ -165,6 +164,7 @@ function MarketPageContent({
[
searchQuery,
componentFilter,
selectedTags,
pageSize,
transformToVO,
plugins.length,
@@ -175,8 +175,34 @@ function MarketPageContent({
// 初始加载
useEffect(() => {
fetchPlugins(1, false, true);
fetchAvailableTags();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// 获取可用标签
const fetchAvailableTags = async () => {
try {
const response = await getCloudServiceClientSync().getAllTags();
const tags = response.tags || [];
setAvailableTags(tags);
// Build tag names map for all components to use
const nameMap: Record<string, string> = {};
tags.forEach((tag: PluginTag) => {
const displayName = {
en_US: tag.display_name.en_US || tag.tag,
zh_Hans: tag.display_name.zh_Hans || tag.tag,
zh_Hant: tag.display_name.zh_Hant,
ja_JP: tag.display_name.ja_JP,
};
nameMap[tag.tag] = extractI18nObject(displayName);
});
setTagNames(nameMap);
} catch (error) {
console.error('Failed to fetch tags:', error);
}
};
// 搜索功能
const handleSearch = useCallback(
(query: string) => {
@@ -227,16 +253,19 @@ function MarketPageContent({
fetchPlugins(1, !!searchQuery.trim(), true);
}, [sortOption, componentFilter]);
// 处理URL参数重定向到 LangBot Space
// Tags 筛选变化时重新搜索
useEffect(() => {
const author = searchParams.get('author');
const pluginName = searchParams.get('plugin');
if (author && pluginName) {
const detailUrl = `https://space.langbot.app/market/${author}/${pluginName}`;
window.open(detailUrl, '_blank');
if (!isLoading) {
setCurrentPage(1);
fetchPlugins(1, searchQuery.trim() !== '', true);
}
}, [searchParams]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedTags]);
// 处理 tags 变化
const handleTagsChange = useCallback((tags: string[]) => {
setSelectedTags(tags);
}, []);
// 处理安装插件
const handleInstallPlugin = useCallback(
@@ -342,8 +371,8 @@ function MarketPageContent({
<div className="h-full flex flex-col">
{/* Fixed header with search and sort controls */}
<div className="flex-shrink-0 space-y-4 px-3 sm:px-4 py-4 sm:py-6">
{/* Search box */}
<div className="flex items-center justify-center">
{/* Search box and Tags filter */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-3">
<div className="relative w-full max-w-2xl">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
<Input
@@ -362,6 +391,13 @@ function MarketPageContent({
className="pl-10 pr-4 text-sm sm:text-base"
/>
</div>
{/* Tags filter */}
<TagsFilter
availableTags={availableTags}
selectedTags={selectedTags}
onTagsChange={handleTagsChange}
/>
</div>
{/* Component filter and sort */}
@@ -460,8 +496,7 @@ function MarketPageContent({
>
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin" />
<span className="ml-2">{t('market.loading')}</span>
<LoadingSpinner text={t('market.loading')} />
</div>
) : plugins.length === 0 ? (
<div className="flex items-center justify-center py-12">
@@ -477,6 +512,7 @@ function MarketPageContent({
key={plugin.pluginId}
cardVO={plugin}
onInstall={handleInstallPlugin}
tagNames={tagNames}
/>
))}
</div>
@@ -484,8 +520,7 @@ function MarketPageContent({
{/* Loading more indicator */}
{isLoadingMore && (
<div className="flex items-center justify-center py-6">
<Loader2 className="h-6 w-6 animate-spin" />
<span className="ml-2">{t('market.loadingMore')}</span>
<LoadingSpinner size="sm" text={t('market.loadingMore')} />
</div>
)}
@@ -522,8 +557,7 @@ export default function MarketPage({
fallback={
<div className="container mx-auto px-4 py-6">
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin" />
<span className="ml-2">...</span>
<LoadingSpinner text="加载中..." />
</div>
</div>
}

View File

@@ -0,0 +1,117 @@
'use client';
import { useTranslation } from 'react-i18next';
import {
Select,
SelectContent,
SelectGroup,
SelectTrigger,
} from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
import { Tag as TagIcon } from 'lucide-react';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { PluginTag } from '@/app/infra/http/CloudServiceClient';
interface TagsFilterProps {
availableTags: PluginTag[];
selectedTags: string[];
onTagsChange: (tags: string[]) => void;
}
export function TagsFilter({
availableTags,
selectedTags,
onTagsChange,
}: TagsFilterProps) {
const { t, i18n } = useTranslation();
const [open, setOpen] = useState(false);
const handleTagToggle = (tag: string) => {
const newTags = selectedTags.includes(tag)
? selectedTags.filter((t) => t !== tag)
: [...selectedTags, tag];
onTagsChange(newTags);
};
const handleClearAll = () => {
onTagsChange([]);
};
const extractI18nObject = (obj: { zh_Hans?: string; en_US?: string }) => {
const lang = i18n.language || 'en_US';
return obj[lang as keyof typeof obj] || obj.zh_Hans || obj.en_US || '';
};
return (
<Select open={open} onOpenChange={setOpen}>
<SelectTrigger className="w-[140px]">
<div className="flex items-center gap-2 w-full">
<TagIcon className="h-4 w-4 flex-shrink-0" />
{selectedTags.length === 0 ? (
<span className="text-muted-foreground truncate text-sm">
{t('market.tags.filterByTags')}
</span>
) : (
<span className="text-sm truncate">
{selectedTags.length} {t('market.tags.selected')}
</span>
)}
</div>
</SelectTrigger>
<SelectContent className="w-[240px]">
<SelectGroup>
<div className="px-2 py-1.5 flex items-center justify-between border-b">
<span className="text-sm font-medium">
{t('market.tags.selectTags')}
</span>
{selectedTags.length > 0 && (
<Button
variant="ghost"
size="sm"
onClick={handleClearAll}
className="h-auto p-0 text-xs hover:bg-transparent hover:text-destructive"
>
{t('market.tags.clearAll')}
</Button>
)}
</div>
{availableTags.length === 0 ? (
<div className="px-2 py-6 text-center text-sm text-muted-foreground">
{t('market.tags.noTags')}
</div>
) : (
<div className="max-h-[300px] overflow-y-auto">
{availableTags.map((tag) => (
<div
key={tag.tag}
className="flex items-center space-x-2 px-2 py-2 hover:bg-accent cursor-pointer"
onClick={(e) => {
e.preventDefault();
handleTagToggle(tag.tag);
}}
>
<Checkbox
id={`tag-${tag.tag}`}
checked={selectedTags.includes(tag.tag)}
onClick={(e) => e.stopPropagation()}
onCheckedChange={() => handleTagToggle(tag.tag)}
/>
<Label
htmlFor={`tag-${tag.tag}`}
className="text-sm font-normal cursor-pointer flex-1"
onClick={(e) => e.preventDefault()}
>
{extractI18nObject(tag.display_name)}
</Label>
</div>
))}
</div>
)}
</SelectGroup>
</SelectContent>
</Select>
);
}

View File

@@ -15,9 +15,11 @@ import { Button } from '@/components/ui/button';
export default function PluginMarketCardComponent({
cardVO,
onInstall,
tagNames = {},
}: {
cardVO: PluginMarketCardVO;
onInstall?: (author: string, pluginName: string) => void;
tagNames?: Record<string, string>;
}) {
const { t } = useTranslation();
const [isHovered, setIsHovered] = useState(false);
@@ -42,13 +44,6 @@ export default function PluginMarketCardComponent({
KnowledgeRetriever: <Book className="w-4 h-4" />,
};
const componentKindNameMap: Record<string, string> = {
Tool: t('plugins.componentName.Tool'),
EventListener: t('plugins.componentName.EventListener'),
Command: t('plugins.componentName.Command'),
KnowledgeRetriever: t('plugins.componentName.KnowledgeRetriever'),
};
return (
<div
className="w-[100%] h-auto min-h-[8rem] sm:min-h-[9rem] bg-white rounded-[10px] shadow-[0px_0px_4px_0_rgba(0,0,0,0.2)] p-3 sm:p-[1rem] hover:shadow-[0px_3px_6px_0_rgba(0,0,0,0.12)] transition-all duration-200 hover:scale-[1.005] dark:bg-[#1f1f22] relative"
@@ -97,24 +92,63 @@ export default function PluginMarketCardComponent({
</div>
</div>
{/* 下部分:下载量和组件列表 */}
<div className="w-full flex flex-row items-center justify-between gap-[0.3rem] sm:gap-[0.4rem] px-0 sm:px-[0.4rem] flex-shrink-0">
<div className="flex flex-row items-center justify-start gap-[0.3rem] sm:gap-[0.4rem]">
<svg
className="w-4 h-4 sm:w-[1.2rem] sm:h-[1.2rem] text-[#2563eb] flex-shrink-0"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7,10 12,15 17,10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
<div className="text-xs sm:text-sm text-[#2563eb] font-medium whitespace-nowrap">
{cardVO.installCount.toLocaleString()}
{/* 下部分:下载量、标签和组件列表 */}
<div className="w-full flex flex-row items-center justify-between gap-2 px-0 sm:px-[0.4rem] flex-shrink-0">
<div className="flex flex-row items-center justify-start gap-2 flex-wrap">
{/* 下载数量 */}
<div className="flex flex-row items-center gap-[0.3rem] sm:gap-[0.4rem]">
<svg
className="w-4 h-4 sm:w-[1.2rem] sm:h-[1.2rem] text-[#2563eb] dark:text-[#5b8def] flex-shrink-0"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7,10 12,15 17,10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
<div className="text-xs sm:text-sm text-[#2563eb] dark:text-[#5b8def] font-medium whitespace-nowrap">
{cardVO.installCount.toLocaleString()}
</div>
</div>
{/* Tags */}
{cardVO.tags && cardVO.tags.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{cardVO.tags.slice(0, 2).map((tag) => (
<Badge
key={tag}
variant="secondary"
className="text-[0.65rem] sm:text-[0.7rem] px-2 py-0.5 h-5 flex items-center gap-1 flex-shrink-0"
>
<svg
className="w-2.5 h-2.5 flex-shrink-0"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z" />
<line x1="7" y1="7" x2="7.01" y2="7" />
</svg>
<span className="truncate">{tagNames[tag] || tag}</span>
</Badge>
))}
{cardVO.tags.length > 2 && (
<Badge
variant="outline"
className="text-[0.65rem] sm:text-[0.7rem] px-2 py-0.5 h-5 flex items-center flex-shrink-0"
>
+{cardVO.tags.length - 2}
</Badge>
)}
</div>
)}
</div>
{/* 组件列表 */}
@@ -127,10 +161,6 @@ export default function PluginMarketCardComponent({
className="flex items-center gap-1"
>
{kindIconMap[kind]}
{/* 响应式显示组件名称:在中等屏幕以上显示 */}
<span className="hidden md:inline">
{componentKindNameMap[kind]}
</span>
<span className="ml-1">{count}</span>
</Badge>
))}

View File

@@ -9,6 +9,7 @@ export interface IPluginMarketCardVO {
githubURL: string;
version: string;
components?: Record<string, number>;
tags?: string[];
}
export class PluginMarketCardVO implements IPluginMarketCardVO {
@@ -22,6 +23,7 @@ export class PluginMarketCardVO implements IPluginMarketCardVO {
installCount: number;
version: string;
components?: Record<string, number>;
tags?: string[];
constructor(prop: IPluginMarketCardVO) {
this.description = prop.description;
@@ -34,5 +36,6 @@ export class PluginMarketCardVO implements IPluginMarketCardVO {
this.pluginId = prop.pluginId;
this.version = prop.version;
this.components = prop.components;
this.tags = prop.tags;
}
}

View File

@@ -35,6 +35,7 @@ export class CloudServiceClient extends BaseHttpClient {
sort_by?: string,
sort_order?: string,
component_filter?: string,
tags_filter?: string[],
): Promise<ApiRespMarketplacePlugins> {
return this.post<ApiRespMarketplacePlugins>(
'/api/v1/marketplace/plugins/search',
@@ -45,6 +46,7 @@ export class CloudServiceClient extends BaseHttpClient {
sort_by,
sort_order,
component_filter,
tags_filter,
},
);
}
@@ -92,6 +94,20 @@ export class CloudServiceClient extends BaseHttpClient {
public getLangBotReleases(): Promise<GitHubRelease[]> {
return this.get<GitHubRelease[]>('/api/v1/dist/info/releases');
}
public getAllTags(): Promise<{ tags: PluginTag[] }> {
return this.get<{ tags: PluginTag[] }>('/api/v1/marketplace/tags');
}
}
export interface PluginTag {
tag: string;
display_name: {
zh_Hans?: string;
en_US?: string;
zh_Hant?: string;
ja_JP?: string;
};
}
export interface GitHubRelease {

View File

@@ -12,6 +12,13 @@ export let systemInfo: ApiRespSystemInfo = {
disable_models_service: false,
};
// 用户信息
export let userInfo: {
user: string;
account_type: 'local' | 'space';
has_password: boolean;
} | null = null;
/**
* 获取基础 URL
*/
@@ -24,6 +31,8 @@ const getBaseURL = (): string => {
// 创建后端客户端实例
export const backendClient = new BackendClient(getBaseURL());
// 为了兼容性,也导出为 httpClient
export const httpClient = backendClient;
// 创建云服务客户端实例(初始化时使用默认 URL
export const cloudServiceClient = new CloudServiceClient(
@@ -82,6 +91,27 @@ export const initializeSystemInfo = async (): Promise<void> => {
}
};
/**
* 初始化用户信息
* 应该在用户登录后调用此方法
*/
export const initializeUserInfo = async (): Promise<void> => {
try {
userInfo = await backendClient.getUserInfo();
} catch (error) {
console.error('Failed to initialize user info:', error);
userInfo = null;
}
};
/**
* 清除用户信息
* 应该在用户登出时调用此方法
*/
export const clearUserInfo = (): void => {
userInfo = null;
};
// 导出类型,以便其他地方使用
export type { ResponseData, RequestConfig } from './BaseHttpClient';
export { BaseHttpClient } from './BaseHttpClient';

View File

@@ -21,7 +21,7 @@ import {
FormMessage,
} from '@/components/ui/form';
import { useEffect, useState } from 'react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { httpClient, initializeUserInfo } from '@/app/infra/http';
import { useRouter } from 'next/navigation';
import { Mail, Lock, Loader2 } from 'lucide-react';
import langbotIcon from '@/app/assets/langbot-logo.webp';
@@ -29,6 +29,7 @@ import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import Link from 'next/link';
import { ThemeToggle } from '@/components/ui/theme-toggle';
import { LoadingSpinner } from '@/components/ui/loading-spinner';
const formSchema = (t: (key: string) => string) =>
z.object({
@@ -95,9 +96,10 @@ export default function Login() {
function handleLogin(username: string, password: string) {
httpClient
.authUser(username, password)
.then((res) => {
.then(async (res) => {
localStorage.setItem('token', res.token);
localStorage.setItem('userEmail', username);
await initializeUserInfo();
router.push('/home');
toast.success(t('common.loginSuccess'));
})
@@ -122,7 +124,7 @@ export default function Login() {
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-neutral-900">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<LoadingSpinner />
</div>
);
}

View File

@@ -0,0 +1,83 @@
import { Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
interface LoadingSpinnerProps {
/**
* Size variant of the spinner
* @default 'default'
*/
size?: 'sm' | 'default' | 'lg';
/**
* Additional CSS classes
*/
className?: string;
/**
* Loading text to display below the spinner
*/
text?: string;
/**
* Whether to display as full page overlay
* @default false
*/
fullPage?: boolean;
}
const sizeMap = {
sm: 'h-4 w-4',
default: 'h-8 w-8',
lg: 'h-12 w-12',
};
const textSizeMap = {
sm: 'text-xs',
default: 'text-sm',
lg: 'text-base',
};
export function LoadingSpinner({
size = 'default',
className,
text = '加载中...',
fullPage = false,
}: LoadingSpinnerProps) {
const spinner = (
<div className="flex flex-col items-center gap-4">
<Loader2
className={cn('animate-spin text-primary', sizeMap[size], className)}
/>
{text && (
<p className={cn('text-muted-foreground', textSizeMap[size])}>{text}</p>
)}
</div>
);
if (fullPage) {
return (
<div className="fixed inset-0 flex items-center justify-center bg-background">
{spinner}
</div>
);
}
return spinner;
}
/**
* Full page loading component for use in page.tsx or layout.tsx
*/
export function LoadingPage({ text }: { text?: string }) {
return <LoadingSpinner fullPage text={text} />;
}
/**
* Inline loading component for use within components
*/
export function LoadingInline({
size,
text,
}: {
size?: 'sm' | 'default' | 'lg';
text?: string;
}) {
return <LoadingSpinner size={size} text={text} />;
}

View File

@@ -446,7 +446,7 @@ const enUS = {
downloadFailed: 'Download failed',
noReadme: 'This plugin does not provide README documentation',
description: 'Description',
tags: 'Tags',
tagLabel: 'Tags',
submissionTitle: 'You have a plugin submission under review: {{name}}',
submissionPending: 'Your plugin submission is under review: {{name}}',
submissionApproved: 'Your plugin submission has been approved: {{name}}',
@@ -462,6 +462,13 @@ const enUS = {
allComponents: 'All Components',
requestPlugin: 'Request Plugin',
viewDetails: 'View Details',
tags: {
filterByTags: 'Filter by Tags',
selected: 'selected',
selectTags: 'Select Tags',
clearAll: 'Clear All',
noTags: 'No tags available',
},
},
mcp: {
title: 'MCP',

View File

@@ -447,7 +447,7 @@ const jaJP = {
downloadFailed: 'ダウンロード失敗',
noReadme: 'このプラグインはREADMEドキュメントを提供していません',
description: '説明',
tags: 'タグ',
tagLabel: 'タグ',
submissionTitle: 'プラグインの提出が審査中です: {{name}}',
submissionPending: 'プラグインの提出が審査中です: {{name}}',
submissionApproved: 'プラグインの提出が承認されました: {{name}}',
@@ -462,6 +462,13 @@ const jaJP = {
filterByComponent: 'コンポーネント',
allComponents: '全部コンポーネント',
requestPlugin: 'プラグインをリクエスト',
tags: {
filterByTags: 'タグで絞り込み',
selected: '選択済み',
selectTags: 'タグを選択',
clearAll: 'クリア',
noTags: 'タグがありません',
},
viewDetails: '詳細を表示',
},
mcp: {

View File

@@ -425,7 +425,7 @@ const zhHans = {
downloadFailed: '下载失败',
noReadme: '该插件没有提供 README 文档',
description: '描述',
tags: '标签',
tagLabel: '标签',
submissionTitle: '您有插件提交正在审核中: {{name}}',
submissionApproved: '您的插件提交已通过审核: {{name}}',
submissionRejected: '您的插件提交已被拒绝: {{name}}',
@@ -439,6 +439,13 @@ const zhHans = {
filterByComponent: '组件',
allComponents: '全部组件',
requestPlugin: '请求插件',
tags: {
filterByTags: '按标签筛选',
selected: '已选',
selectTags: '选择标签',
clearAll: '清空',
noTags: '暂无标签',
},
viewDetails: '查看详情',
},
mcp: {

View File

@@ -418,7 +418,7 @@ const zhHant = {
downloadFailed: '下載失敗',
noReadme: '該插件沒有提供 README 文件',
description: '描述',
tags: '標籤',
tagLabel: '標籤',
submissionTitle: '您有插件提交正在審核中: {{name}}',
submissionApproved: '您的插件提交已通過審核: {{name}}',
submissionRejected: '您的插件提交已被拒絕: {{name}}',
@@ -432,6 +432,13 @@ const zhHant = {
filterByComponent: '組件',
allComponents: '全部組件',
requestPlugin: '請求插件',
tags: {
filterByTags: '按標籤篩選',
selected: '已選',
selectTags: '選擇標籤',
clearAll: '清空',
noTags: '暫無標籤',
},
viewDetails: '查看詳情',
},
mcp: {

View File

@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -11,7 +15,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
@@ -19,9 +23,19 @@
}
],
"paths": {
"@/*": ["./src/*"]
"@/*": [
"./src/*"
]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}