From fec62ae581f0c11d1fb2ff1d47ae8f8e21cdb9f0 Mon Sep 17 00:00:00 2001
From: xiaoxing0135 <706015750@qq.com>
Date: Sat, 16 May 2026 17:51:33 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20GUI=20polish=20=E2=80=94=20Settings,=20?=
=?UTF-8?q?Doctor,=20onboarding,=20export,=20version=20sync?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Settings page: LLM provider + API key + model picker (7 providers)
- Doctor page: health/catalog/LLM config self-check
- Onboarding: first-visit quick start guide on Upload page
- Report page: JSON export download button
- Navigation: new Settings + Check tabs in header
- Version sync: App.tsx v1.32.0, VERSION 1.32.0
- Windows CI: fix artifact glob (*.exe)
---
.github/workflows/desktop-release.yml | 4 +-
VERSION | 2 +-
runtime/web/src/App.tsx | 24 ++++--
runtime/web/src/main.tsx | 4 +
runtime/web/src/pages/DoctorPage.tsx | 100 +++++++++++++++++++++++++
runtime/web/src/pages/ReportPage.tsx | 25 ++++++-
runtime/web/src/pages/SettingsPage.tsx | 99 ++++++++++++++++++++++++
runtime/web/src/pages/UploadPage.tsx | 22 +++++-
8 files changed, 266 insertions(+), 14 deletions(-)
create mode 100644 runtime/web/src/pages/DoctorPage.tsx
create mode 100644 runtime/web/src/pages/SettingsPage.tsx
diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml
index 87fb27d..c128b39 100644
--- a/.github/workflows/desktop-release.yml
+++ b/.github/workflows/desktop-release.yml
@@ -49,7 +49,7 @@ jobs:
- uses: actions/upload-artifact@v4
with:
name: Test-Agent-Windows
- path: dist-electron/Test-Agent-Setup-*.exe
+ path: dist-electron/*.exe
build-macos:
runs-on: macos-latest
@@ -105,7 +105,7 @@ jobs:
uses: softprops/action-gh-release@v2
with:
files: |
- Test-Agent-Windows/Test-Agent-Setup-*.exe
+ Test-Agent-Windows/*.exe
Test-Agent-macOS/Test-Agent-*.dmg
draft: true
generate_release_notes: true
diff --git a/VERSION b/VERSION
index 34aae15..359c410 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.31.0
+1.32.0
diff --git a/runtime/web/src/App.tsx b/runtime/web/src/App.tsx
index 071abcf..c2bb3e4 100644
--- a/runtime/web/src/App.tsx
+++ b/runtime/web/src/App.tsx
@@ -1,5 +1,5 @@
import { Outlet, NavLink } from "react-router-dom";
-import { Beaker, Upload, BookOpen } from "lucide-react";
+import { Beaker, Upload, BookOpen, Settings, Stethoscope } from "lucide-react";
export default function App() {
return (
@@ -8,21 +8,33 @@ export default function App() {
- Test-Agent · Runtime
- v1.31.0
+ Test-Agent
+ v1.32.0
(isActive ? "font-semibold" : "")}>
- 上传
+ Upload
(isActive ? "font-semibold" : "")}>
- 目录
+ Catalog
+
+
+
+ (isActive ? "font-semibold" : "")}>
+
+ Check
+
+
+
+ (isActive ? "font-semibold" : "")}>
+
+ Settings
@@ -34,7 +46,7 @@ export default function App() {
diff --git a/runtime/web/src/main.tsx b/runtime/web/src/main.tsx
index 89af64b..7c9209d 100644
--- a/runtime/web/src/main.tsx
+++ b/runtime/web/src/main.tsx
@@ -7,6 +7,8 @@ import UploadPage from "./pages/UploadPage";
import RunStatusPage from "./pages/RunStatusPage";
import ReportPage from "./pages/ReportPage";
import CatalogPage from "./pages/CatalogPage";
+import SettingsPage from "./pages/SettingsPage";
+import DoctorPage from "./pages/DoctorPage";
import "./index.css";
const queryClient = new QueryClient({
@@ -23,6 +25,8 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
} />
} />
} />
+ } />
+ } />
} />
diff --git a/runtime/web/src/pages/DoctorPage.tsx b/runtime/web/src/pages/DoctorPage.tsx
new file mode 100644
index 0000000..a7d0a41
--- /dev/null
+++ b/runtime/web/src/pages/DoctorPage.tsx
@@ -0,0 +1,100 @@
+import { useEffect, useState } from "react";
+import { Stethoscope, Check, X, Loader2 } from "lucide-react";
+
+interface CheckItem {
+ name: string;
+ status: "pending" | "ok" | "skip" | "fail";
+ detail: string;
+}
+
+export default function DoctorPage() {
+ const [checks, setChecks] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ runDoctor();
+ }, []);
+
+ const runDoctor = async () => {
+ setLoading(true);
+ const results: CheckItem[] = [];
+
+ // Catalog check
+ try {
+ const res = await fetch("http://localhost:8800/catalog");
+ if (res.ok) {
+ const data = await res.json();
+ results.push({ name: "Catalog", status: "ok", detail: `${data.counts?.experts || 0} experts + ${data.counts?.skills || 0} skills` });
+ } else {
+ results.push({ name: "Catalog", status: "fail", detail: `HTTP ${res.status}` });
+ }
+ } catch {
+ results.push({ name: "Catalog", status: "fail", detail: "Backend not reachable" });
+ }
+
+ // Health check
+ try {
+ const res = await fetch("http://localhost:8800/health");
+ if (res.ok) {
+ const data = await res.json();
+ results.push({ name: "Backend", status: "ok", detail: `v${data.version}` });
+ } else {
+ results.push({ name: "Backend", status: "fail", detail: `HTTP ${res.status}` });
+ }
+ } catch {
+ results.push({ name: "Backend", status: "fail", detail: "Not running — start backend first" });
+ }
+
+ // Settings check
+ const provider = localStorage.getItem("tagent_provider") || "stub";
+ const hasKey = !!localStorage.getItem("tagent_api_key");
+ if (provider === "stub") {
+ results.push({ name: "LLM Config", status: "skip", detail: "Stub mode (offline demo)" });
+ } else if (!hasKey) {
+ results.push({ name: "LLM Config", status: "fail", detail: `Provider: ${provider}, no API key set` });
+ } else {
+ results.push({ name: "LLM Config", status: "ok", detail: `Provider: ${provider}` });
+ }
+
+ setChecks(results);
+ setLoading(false);
+ };
+
+ const statusIcon = (s: string) => {
+ switch (s) {
+ case "ok": return ;
+ case "fail": return ;
+ case "skip": return SKIP ;
+ default: return ;
+ }
+ };
+
+ return (
+
+
+
+ System Check
+
+
+ {loading ? "Checking..." : "Re-run"}
+
+
+
+
+ {checks.map((c) => (
+
+
+
{c.name}
+
{c.detail}
+
+ {statusIcon(c.status)}
+
+ ))}
+
+
+ {!loading && checks.length === 0 && (
+
No checks run. Click "Re-run".
+ )}
+
+ );
+}
diff --git a/runtime/web/src/pages/ReportPage.tsx b/runtime/web/src/pages/ReportPage.tsx
index 4726baa..80129c1 100644
--- a/runtime/web/src/pages/ReportPage.tsx
+++ b/runtime/web/src/pages/ReportPage.tsx
@@ -1,7 +1,18 @@
import { useParams, Link } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
+import { Download } from "lucide-react";
import { getReport } from "@/api";
+function downloadJSON(data: unknown, filename: string) {
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = filename;
+ a.click();
+ URL.revokeObjectURL(url);
+}
+
export default function ReportPage() {
const { run_id } = useParams<{ run_id: string }>();
const query = useQuery({
@@ -24,9 +35,17 @@ export default function ReportPage() {
return (
-
- Report · {run_id}
-
+
+
+ Report · {run_id}
+
+ downloadJSON(report, `tagent-report-${run_id}.json`)}
+ className="flex items-center gap-1.5 px-3 py-1.5 text-sm border rounded-lg hover:bg-slate-50"
+ >
+ Export JSON
+
+
succeeded={String(report.succeeded ?? 0)}
diff --git a/runtime/web/src/pages/SettingsPage.tsx b/runtime/web/src/pages/SettingsPage.tsx
new file mode 100644
index 0000000..c6697b4
--- /dev/null
+++ b/runtime/web/src/pages/SettingsPage.tsx
@@ -0,0 +1,99 @@
+import { useState } from "react";
+import { Settings, Key, Cpu, Save, Check } from "lucide-react";
+
+const PROVIDERS = [
+ { id: "claude", label: "Claude (Anthropic)", models: ["claude-sonnet-4-6", "claude-opus-4-7", "claude-haiku-4-5"] },
+ { id: "openai", label: "OpenAI", models: ["gpt-4o", "gpt-4o-mini"] },
+ { id: "deepseek", label: "DeepSeek", models: ["deepseek-chat", "deepseek-reasoner"] },
+ { id: "qwen", label: "Qwen", models: ["qwen-plus", "qwen-max"] },
+ { id: "gemini", label: "Gemini (Google)", models: ["gemini-2.5-pro", "gemini-2.5-flash"] },
+ { id: "ollama", label: "Ollama (Local)", models: ["qwen2.5:7b", "llama3.2:3b"] },
+ { id: "stub", label: "Stub (Offline/Demo)", models: ["stub"] },
+];
+
+export default function SettingsPage() {
+ const [provider, setProvider] = useState(localStorage.getItem("tagent_provider") || "stub");
+ const [apiKey, setApiKey] = useState(localStorage.getItem("tagent_api_key") || "");
+ const [model, setModel] = useState(localStorage.getItem("tagent_model") || "stub");
+ const [saved, setSaved] = useState(false);
+
+ const currentProvider = PROVIDERS.find((p) => p.id === provider)!;
+
+ const save = () => {
+ localStorage.setItem("tagent_provider", provider);
+ localStorage.setItem("tagent_api_key", apiKey);
+ localStorage.setItem("tagent_model", model);
+ setSaved(true);
+ setTimeout(() => setSaved(false), 2000);
+ };
+
+ return (
+
+
+ Settings
+
+
+ {/* LLM Provider */}
+
+
+ LLM Provider
+
+
+ {PROVIDERS.map((p) => (
+
{ setProvider(p.id); setModel(p.models[0]); }}
+ className={`text-left px-3 py-2 rounded-lg border text-sm transition
+ ${provider === p.id ? "border-blue-500 bg-blue-50 ring-1 ring-blue-500" : "border-slate-200 hover:border-slate-300"}`}
+ >
+ {p.label}
+ {p.models.length} models
+
+ ))}
+
+
+
+ {/* API Key */}
+
+
+ {/* Model */}
+
+ Model
+ setModel(e.target.value)}
+ className="w-full px-3 py-2 border rounded-lg text-sm"
+ >
+ {currentProvider.models.map((m) => (
+ {m}
+ ))}
+
+
+
+ {/* Save */}
+
+ {saved ? : }
+ {saved ? "Saved" : "Save Settings"}
+
+
+
+ Settings are stored locally. {provider === "stub" ? "Switch to a real provider and add your API key for AI-powered testing." : "Your API key is stored only in this browser/app."}
+
+
+ );
+}
diff --git a/runtime/web/src/pages/UploadPage.tsx b/runtime/web/src/pages/UploadPage.tsx
index 53d4488..58d5265 100644
--- a/runtime/web/src/pages/UploadPage.tsx
+++ b/runtime/web/src/pages/UploadPage.tsx
@@ -1,12 +1,14 @@
-import { useState, FormEvent } from "react";
+import { useState, FormEvent, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useMutation } from "@tanstack/react-query";
import { postRunFile, postRunText, postRunUrl, RunCreated } from "@/api";
+import { Lightbulb, X } from "lucide-react";
type Mode = "text" | "file" | "url";
export default function UploadPage() {
const [mode, setMode] = useState("text");
+ const [showGuide, setShowGuide] = useState(!localStorage.getItem("tagent_onboarded"));
const [text, setText] = useState("");
const [url, setUrl] = useState("");
const [file, setFile] = useState(null);
@@ -33,10 +35,26 @@ export default function UploadPage() {
新建测试任务
-
+
支持文本指令、文件上传(PDF/Word/MD/exe/APK/IPA/Docker)、或被测系统 URL。
+ {showGuide && (
+
+
+ Quick Start
+ { setShowGuide(false); localStorage.setItem("tagent_onboarded", "1"); }} className="text-slate-400 hover:text-slate-600">
+
+
+ Enter a test target (e.g. "Login page at https://example.com")
+ Click Start Test — AI plans & runs the test
+ Watch progress in real-time → view report
+ Go to Settings to add your LLM API key for smarter results
+ Check System Check to verify everything works
+
+
+ )}
+
输入方式