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() {

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 +

+ +
+ +
+ {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} +

+ +
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) => ( + + ))} +
+
+ + {/* API Key */} +
+

+ API Key {provider === "stub" || provider === "ollama" ? "(optional)" : ""} +

+ setApiKey(e.target.value)} + placeholder={provider === "stub" ? "No key needed (offline demo)" : "sk-..."} + className="w-full px-3 py-2 border rounded-lg text-sm font-mono" + disabled={provider === "stub"} + /> +
+ + {/* Model */} +
+

Model

+ +
+ + {/* Save */} + + +

+ 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 + +
+
    +
  1. Enter a test target (e.g. "Login page at https://example.com")
  2. +
  3. Click Start Test — AI plans & runs the test
  4. +
  5. Watch progress in real-time → view report
  6. +
  7. Go to Settings to add your LLM API key for smarter results
  8. +
  9. Check System Check to verify everything works
  10. +
+
+ )} +
输入方式