This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new 19d6cafb fix: load home AI models from LLM settings (#689)
19d6cafb is described below
commit 19d6cafb598e99b4f211a859beda022c1f7e111c
Author: aias00 <[email protected]>
AuthorDate: Sun Aug 2 19:37:28 2026 -0700
fix: load home AI models from LLM settings (#689)
---
web/src/pages/home/__tests__/HomePage.test.tsx | 104 +++++++++++++++++++++++++
web/src/pages/home/index.tsx | 55 ++++++++++---
2 files changed, 149 insertions(+), 10 deletions(-)
diff --git a/web/src/pages/home/__tests__/HomePage.test.tsx
b/web/src/pages/home/__tests__/HomePage.test.tsx
new file mode 100644
index 00000000..c2ce3cd2
--- /dev/null
+++ b/web/src/pages/home/__tests__/HomePage.test.tsx
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { App } from 'antd';
+import { LangProvider } from '../../../i18n/LangContext';
+import HomePage from '../index';
+
+const navigateMock = vi.hoisted(() => vi.fn());
+const llmApiMocks = vi.hoisted(() => ({
+ getLlmConfig: vi.fn(),
+ getLlmModels: vi.fn(),
+}));
+
+vi.mock('react-router-dom', () => ({
+ useNavigate: () => navigateMock,
+}));
+
+vi.mock('../../../api/llm', () => llmApiMocks);
+
+beforeAll(() => {
+ Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ value: vi.fn().mockImplementation((query: string) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+ });
+});
+
+beforeEach(() => {
+ vi.clearAllMocks();
+ llmApiMocks.getLlmConfig.mockResolvedValue({
+ provider: 'openai',
+ apiBase: 'https://api.example.com/v1',
+ model: 'provider-model',
+ maxTokens: 4096,
+ temperature: 0.7,
+ enabled: true,
+ ready: true,
+ });
+ llmApiMocks.getLlmModels.mockResolvedValue({
+ status: 0,
+ data: [{ id: 'provider-model' }, { id: 'provider-model-secondary' }],
+ source: 'provider',
+ });
+});
+
+const renderHome = () =>
+ render(
+ <App>
+ <LangProvider>
+ <HomePage />
+ </LangProvider>
+ </App>,
+ );
+
+describe('HomePage LLM models', () => {
+ it('loads configured provider models instead of hard-coded options', async
() => {
+ renderHome();
+
+ expect(await screen.findByText('provider-model')).toBeInTheDocument();
+ expect(screen.queryByText('qwen3.7-max')).not.toBeInTheDocument();
+ });
+
+ it('submits the configured provider model to the AI page', async () => {
+ const user = userEvent.setup();
+ renderHome();
+ await screen.findByText('provider-model');
+
+ await user.type(screen.getByPlaceholderText('向 RocketMQ Bot
提问,全程加密、安全、可信'), '查看集群状态{enter}');
+
+ await waitFor(() => {
+ expect(navigateMock).toHaveBeenCalledWith('/ai', {
+ state: {
+ prompt: '查看集群状态',
+ model: 'provider-model',
+ },
+ });
+ });
+ });
+});
diff --git a/web/src/pages/home/index.tsx b/web/src/pages/home/index.tsx
index a83e3095..ae7735a8 100644
--- a/web/src/pages/home/index.tsx
+++ b/web/src/pages/home/index.tsx
@@ -32,6 +32,7 @@ import {
MegaphoneSimple,
Database,
} from '@phosphor-icons/react';
+import { getLlmConfig, getLlmModels } from '../../api/llm';
import { useLang } from '../../i18n/LangContext';
/* ─── Time-aware greeting key ─── */
@@ -52,20 +53,18 @@ const modes = [
{ key: 'chat', labelKey: 'home.mode.chat', icon: ChatCircleDots },
];
-/* ─── Model option values ─── */
-const modelOptions = [
- { value: 'qwen3.7-max', recommended: true },
- { value: 'qwen3.7-plus', recommended: false },
- { value: 'claude-opus-4.7', recommended: false },
- { value: 'gpt-5.4', recommended: false },
-];
+interface ModelOption {
+ value: string;
+ recommended: boolean;
+}
/* ═══════════════════════════════════════════════════════
HomePage Component
═══════════════════════════════════════════════════════ */
const HomePage = () => {
const [activeMode, setActiveMode] = useState('query');
- const [selectedModel, setSelectedModel] = useState('qwen3.7-max');
+ const [modelOptions, setModelOptions] = useState<ModelOption[]>([]);
+ const [selectedModel, setSelectedModel] = useState('');
const [inputValue, setInputValue] = useState('');
const [indicatorStyle, setIndicatorStyle] = useState({ width: 83, left: 6 });
const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -73,6 +72,39 @@ const HomePage = () => {
const navigate = useNavigate();
const { t, lang } = useLang();
+ useEffect(() => {
+ let cancelled = false;
+
+ const loadModels = async () => {
+ const [config, modelsResult] = await Promise.all([
+ getLlmConfig().catch(() => null),
+ getLlmModels().catch(() => null),
+ ]);
+ if (cancelled) return;
+
+ const configuredModel = config?.model?.trim() ?? '';
+ const providerModels = modelsResult?.status === 0 && modelsResult.data
+ ? modelsResult.data.map((item) => item.id || item.name ||
'').filter(Boolean)
+ : [];
+ const values = Array.from(new Set([
+ ...(configuredModel ? [configuredModel] : []),
+ ...providerModels,
+ ]));
+
+ setModelOptions(values.map((value, index) => ({
+ value,
+ recommended: configuredModel ? value === configuredModel : index === 0,
+ })));
+ setSelectedModel((current) => (current && values.includes(current) ?
current : values[0] || ''));
+ };
+
+ void loadModels();
+
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
/* ─── Mode switch handler ─── */
const handleModeSwitch = useCallback((key: string, btn: HTMLButtonElement)
=> {
setActiveMode(key);
@@ -124,7 +156,7 @@ const HomePage = () => {
const handlePromptSubmit = () => {
const prompt = inputValue.trim();
- navigate('/ai', { state: prompt ? { prompt, model: selectedModel } : null
});
+ navigate('/ai', { state: prompt ? { prompt, ...(selectedModel ? { model:
selectedModel } : {}) } : null });
};
return (
@@ -292,8 +324,11 @@ const HomePage = () => {
<div className="flex flex-1 min-w-0 items-center gap-2">
<Select
size="small"
- value={selectedModel}
+ value={selectedModel || undefined}
onChange={(val) => setSelectedModel(val)}
+ disabled={modelOptions.length === 0}
+ placeholder={lang === 'zh' ? '选择模型' : 'Select model'}
+ notFoundContent={lang === 'zh' ? '未配置模型' : 'No
configured models'}
options={modelOptions.map((m) => ({
value: m.value,
label: m.recommended ? (