mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 12:05:54 +00:00
Compare commits
15 Commits
v4.8.1
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e0275ee08 | ||
|
|
3fa7389ba9 | ||
|
|
84a3b1f465 | ||
|
|
377d011da2 | ||
|
|
e0d72969e3 | ||
|
|
a65b7ad413 | ||
|
|
45df44e01b | ||
|
|
d8addb105a | ||
|
|
f17ccad665 | ||
|
|
120ceb0b55 | ||
|
|
8a6f80a181 | ||
|
|
b19e468668 | ||
|
|
aeac79e1b3 | ||
|
|
b89a240250 | ||
|
|
13f42857f5 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -42,7 +42,6 @@ botpy.log*
|
||||
test.py
|
||||
/web_ui
|
||||
.venv/
|
||||
uv.lock
|
||||
/test
|
||||
plugins.bak
|
||||
coverage.xml
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -20,6 +20,24 @@ class GeminiChatCompletions(chatcmpl.OpenAIChatCompletions):
|
||||
'timeout': 120,
|
||||
}
|
||||
|
||||
def _add_thought_signature_to_messages(self, messages: list[dict]) -> list[dict]:
|
||||
"""Add thought_signature to tool_calls in messages for Gemini API compatibility
|
||||
|
||||
Gemini API requires a thought_signature field in function call parts.
|
||||
See: https://ai.google.dev/gemini-api/docs/thought-signatures
|
||||
|
||||
Note: This function modifies the dictionaries in the messages list in place.
|
||||
"""
|
||||
for msg in messages:
|
||||
if 'tool_calls' in msg and msg['tool_calls']:
|
||||
# Ensure we're working with a mutable copy of tool_calls
|
||||
if not isinstance(msg['tool_calls'], list):
|
||||
continue
|
||||
for tool_call in msg['tool_calls']:
|
||||
if isinstance(tool_call, dict) and 'thought_signature' not in tool_call:
|
||||
tool_call['thought_signature'] = ''
|
||||
return messages
|
||||
|
||||
async def _closure_stream(
|
||||
self,
|
||||
query: pipeline_query.Query,
|
||||
@@ -42,6 +60,9 @@ class GeminiChatCompletions(chatcmpl.OpenAIChatCompletions):
|
||||
# 设置此次请求中的messages
|
||||
messages = req_messages.copy()
|
||||
|
||||
# Add thought_signature to tool_calls for Gemini compatibility
|
||||
messages = self._add_thought_signature_to_messages(messages)
|
||||
|
||||
# 检查vision
|
||||
for msg in messages:
|
||||
if 'content' in msg and isinstance(msg['content'], list):
|
||||
@@ -140,3 +161,29 @@ class GeminiChatCompletions(chatcmpl.OpenAIChatCompletions):
|
||||
|
||||
yield provider_message.MessageChunk(**chunk_data)
|
||||
chunk_idx += 1
|
||||
|
||||
async def _closure(
|
||||
self,
|
||||
query: pipeline_query.Query,
|
||||
req_messages: list[dict],
|
||||
use_model: requester.RuntimeLLMModel,
|
||||
use_funcs: list[resource_tool.LLMTool] = None,
|
||||
extra_args: dict[str, typing.Any] = {},
|
||||
remove_think: bool = False,
|
||||
) -> tuple[provider_message.Message, dict]:
|
||||
"""Override _closure to add thought_signature to messages"""
|
||||
# Make a shallow copy to avoid mutating the caller's list
|
||||
messages = req_messages.copy()
|
||||
|
||||
# Add thought_signature to tool_calls for Gemini compatibility
|
||||
messages = self._add_thought_signature_to_messages(messages)
|
||||
|
||||
# Call parent implementation
|
||||
return await super()._closure(
|
||||
query=query,
|
||||
req_messages=messages,
|
||||
use_model=use_model,
|
||||
use_funcs=use_funcs,
|
||||
extra_args=extra_args,
|
||||
remove_think=remove_think,
|
||||
)
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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
100
web/pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
|
||||
@@ -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}>
|
||||
|
||||
5
web/src/app/home/loading.tsx
Normal file
5
web/src/app/home/loading.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { LoadingPage } from '@/components/ui/loading-spinner';
|
||||
|
||||
export default function Loading() {
|
||||
return <LoadingPage />;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
117
web/src/app/home/plugins/components/plugin-market/TagsFilter.tsx
Normal file
117
web/src/app/home/plugins/components/plugin-market/TagsFilter.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
83
web/src/components/ui/loading-spinner.tsx
Normal file
83
web/src/components/ui/loading-spinner.tsx
Normal 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} />;
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user