Compare commits

..

2 Commits

Author SHA1 Message Date
Xin Wang
86d499245c Add Tauri desktop scaffolding and improve response rendering.
This updates project metadata/scripts for desktop builds, adds Tauri app files, refreshes release artifacts, and improves results preview handling for structured and think-tagged responses.

Made-with: Cursor
2026-04-30 13:57:58 +08:00
Xin Wang
f0aeb22f77 Improve ID generation fallback and enable trace capture by default.
Add robust UUID fallback logic across test case creation paths, default trace capture to on, and include generated release artifacts.

Made-with: Cursor
2026-04-29 14:07:13 +08:00
71 changed files with 16428 additions and 12 deletions

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@ node_modules/
build/
dist/
coverage/
src-tauri/target/
.DS_Store
*.log
.env*

View File

@@ -25,4 +25,26 @@ A simple web app for batch testing prompts/workflows.
- `npm run dev` - start development server
- `npm run build` - build for production
- `npm run preview` - preview production build
- `npm run desktop:dev` - start the Tauri desktop app in development
- `npm run desktop:build` - build the Tauri desktop app
- `npm run lint` - run TypeScript checks
## Desktop App
This project includes a Tauri v2 wrapper in `src-tauri/`.
Before running the desktop app, install the Tauri system prerequisites for your OS:
[Tauri prerequisites](https://v2.tauri.app/start/prerequisites/).
On macOS, that includes Rust/Cargo and Xcode command line tools. After prerequisites are installed:
```bash
npm install
npm run desktop:dev
```
To create a desktop bundle:
```bash
npm run desktop:build
```

BIN
dist.zip Normal file

Binary file not shown.

237
package-lock.json generated
View File

@@ -1,11 +1,11 @@
{
"name": "react-example",
"name": "dify-batch-tester",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "react-example",
"name": "dify-batch-tester",
"version": "0.0.0",
"dependencies": {
"@base-ui/react": "^1.4.0",
@@ -32,6 +32,7 @@
"xlsx": "^0.18.5"
},
"devDependencies": {
"@tauri-apps/cli": "^2.10.1",
"@types/express": "^4.17.21",
"@types/node": "^22.14.0",
"autoprefixer": "^10.4.21",
@@ -2417,6 +2418,238 @@
"vite": "^5.2.0 || ^6 || ^7 || ^8"
}
},
"node_modules/@tauri-apps/cli": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.1.tgz",
"integrity": "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==",
"dev": true,
"license": "Apache-2.0 OR MIT",
"bin": {
"tauri": "tauri.js"
},
"engines": {
"node": ">= 10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/tauri"
},
"optionalDependencies": {
"@tauri-apps/cli-darwin-arm64": "2.10.1",
"@tauri-apps/cli-darwin-x64": "2.10.1",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1",
"@tauri-apps/cli-linux-arm64-gnu": "2.10.1",
"@tauri-apps/cli-linux-arm64-musl": "2.10.1",
"@tauri-apps/cli-linux-riscv64-gnu": "2.10.1",
"@tauri-apps/cli-linux-x64-gnu": "2.10.1",
"@tauri-apps/cli-linux-x64-musl": "2.10.1",
"@tauri-apps/cli-win32-arm64-msvc": "2.10.1",
"@tauri-apps/cli-win32-ia32-msvc": "2.10.1",
"@tauri-apps/cli-win32-x64-msvc": "2.10.1"
}
},
"node_modules/@tauri-apps/cli-darwin-arm64": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz",
"integrity": "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-darwin-x64": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz",
"integrity": "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz",
"integrity": "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==",
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz",
"integrity": "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz",
"integrity": "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz",
"integrity": "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==",
"cpu": [
"riscv64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz",
"integrity": "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-linux-x64-musl": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz",
"integrity": "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz",
"integrity": "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz",
"integrity": "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz",
"integrity": "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 OR MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@ts-morph/common": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz",

View File

@@ -1,12 +1,16 @@
{
"name": "react-example",
"name": "dify-batch-tester",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --port=3000 --host=0.0.0.0",
"dev:tauri": "vite --port=3000 --host=127.0.0.1 --strictPort",
"build": "vite build",
"preview": "vite preview",
"tauri": "tauri",
"desktop:dev": "tauri dev",
"desktop:build": "tauri build",
"clean": "rm -rf dist",
"lint": "tsc --noEmit"
},
@@ -35,6 +39,7 @@
"xlsx": "^0.18.5"
},
"devDependencies": {
"@tauri-apps/cli": "^2.10.1",
"@types/express": "^4.17.21",
"@types/node": "^22.14.0",
"autoprefixer": "^10.4.21",

Binary file not shown.

Binary file not shown.

5479
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

17
src-tauri/Cargo.toml Normal file
View File

@@ -0,0 +1,17 @@
[package]
name = "dify-batch-tester"
version = "0.0.0"
description = "Dify Batch Tester desktop app"
authors = ["wangx"]
edition = "2021"
[lib]
name = "dify_batch_tester_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"

3
src-tauri/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

BIN
src-tauri/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

7
src-tauri/src/lib.rs Normal file
View File

@@ -0,0 +1,7 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

3
src-tauri/src/main.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
dify_batch_tester_lib::run()
}

31
src-tauri/tauri.conf.json Normal file
View File

@@ -0,0 +1,31 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Dify Batch Tester",
"version": "0.0.0",
"identifier": "com.wangx.dify-batch-tester",
"build": {
"beforeDevCommand": "npm run dev:tauri",
"devUrl": "http://localhost:3000",
"beforeBuildCommand": "npm run build",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "Dify Batch Tester",
"width": 1280,
"height": 860,
"minWidth": 1024,
"minHeight": 700,
"resizable": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all"
}
}

View File

@@ -38,6 +38,26 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Di
import { DifyInput, TestCase, DifyConfig, AppMode, NodeTraceStep, NodeStatus, TestResult } from './types';
const createId = () => {
if (globalThis.crypto?.randomUUID) {
return globalThis.crypto.randomUUID();
}
if (globalThis.crypto?.getRandomValues) {
const bytes = new Uint8Array(16);
globalThis.crypto.getRandomValues(bytes);
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
return Array.from(bytes, (byte, index) => {
const hex = byte.toString(16).padStart(2, '0');
return [4, 6, 8, 10].includes(index) ? `-${hex}` : hex;
}).join('');
}
return `id-${Date.now()}-${Math.random().toString(36).slice(2)}`;
};
// Parse a Dify SSE stream (workflow/advanced-chat) into a TestResult.
// Emits live updates via onUpdate after each meaningful event so the UI
// can reflect node-level progress as it arrives.
@@ -202,7 +222,7 @@ export default function App() {
const [testCases, setTestCases] = useState<TestCase[]>([]);
const [isExecuting, setIsExecuting] = useState(false);
const [isStopping, setIsStopping] = useState(false);
const [captureTrace, setCaptureTrace] = useState(false);
const [captureTrace, setCaptureTrace] = useState(true);
const [traceDialogResult, setTraceDialogResult] = useState<TestResult | null>(null);
const [difyConfig, setDifyConfig] = useState<DifyConfig>({
apiKey: '',
@@ -256,7 +276,7 @@ export default function App() {
if (response.status === 404) {
setInputs([]);
setTestCases([{
id: crypto.randomUUID(),
id: createId(),
inputs: {},
times: 1,
status: 'idle',
@@ -365,7 +385,7 @@ export default function App() {
});
return {
id: crypto.randomUUID(),
id: createId(),
inputs: testCaseInputs,
query: difyConfig.appMode === 'chat' ? (row['query'] || row['Query'] || '') : undefined,
times: parseInt(row['times'] || row['Times'] || '1') || 1,
@@ -389,7 +409,7 @@ export default function App() {
const addTestCase = () => {
const newCase: TestCase = {
id: crypto.randomUUID(),
id: createId(),
inputs: {},
query: difyConfig.appMode === 'chat' ? '' : undefined,
times: 1,
@@ -838,7 +858,7 @@ export default function App() {
return testCase.results.map((result, rIndex) => {
const outputs = result.error
? result.error
: (result.data?.outputs || result.answer || JSON.stringify(result));
: (result.data?.outputs ?? result.answer ?? result);
const outputString = typeof outputs === 'object'
? JSON.stringify(outputs, null, 2)
@@ -851,6 +871,7 @@ export default function App() {
query: testCase.query,
isError: !!result.error,
output: outputString,
outputRaw: outputs,
taskId: result.task_id || result.id || 'N/A',
result
};
@@ -932,9 +953,7 @@ export default function App() {
</TableCell>
)}
<TableCell className="px-4 py-4 align-top">
<div className="w-[300px] max-w-[300px] max-h-40 overflow-auto whitespace-pre-wrap break-all font-mono text-[10px] leading-relaxed bg-slate-50 p-3 rounded-xl border border-slate-100 text-[#334155] shadow-[inset_0_1px_2px_rgba(0,0,0,0.02)]">
{row.output}
</div>
<ResponsePreview value={row.outputRaw} />
</TableCell>
</TableRow>
);
@@ -1145,7 +1164,7 @@ export default function App() {
<Button
onClick={() => {
setTestCases([{
id: crypto.randomUUID(),
id: createId(),
inputs: Object.fromEntries(inputs.map(i => [i.name, ''])),
times: 1,
status: 'idle',
@@ -1637,6 +1656,175 @@ function safeStringify(v: unknown): string {
}
}
type ResponsePart = {
type: 'answer' | 'think';
content: string;
};
function decodeEscapedResponseText(value: string): string {
return value
.replace(/\\r\\n/g, '\n')
.replace(/\\n/g, '\n')
.replace(/\\t/g, '\t')
.replace(/\\"/g, '"');
}
function splitResponseParts(value: string): ResponsePart[] {
const text = decodeEscapedResponseText(value);
const parts: ResponsePart[] = [];
const thinkBlockPattern = /<think>([\s\S]*?)(?:<\/think>|$)/gi;
let lastIndex = 0;
for (const match of text.matchAll(thinkBlockPattern)) {
const matchIndex = match.index ?? 0;
const answer = text.slice(lastIndex, matchIndex);
if (answer.trim()) {
parts.push({ type: 'answer', content: answer });
}
if (match[1]?.trim()) {
parts.push({ type: 'think', content: match[1] });
}
lastIndex = matchIndex + match[0].length;
}
const answer = text.slice(lastIndex);
if (answer.trim()) {
parts.push({ type: 'answer', content: answer });
}
return parts.length ? parts : [{ type: 'answer', content: text }];
}
function parseJsonLikeString(value: string): unknown | undefined {
const text = decodeEscapedResponseText(value).trim();
if (!text || !/^[{[]/.test(text)) return undefined;
try {
return JSON.parse(text);
} catch {
return undefined;
}
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function ResponseText({ value }: { value: string }) {
// Plain <div>s (not <pre>) so the surrounding context controls the font:
// sans-serif when used as a top-level answer, monospace when nested
// inside a structured/JSON panel that already sets font-mono.
return (
<>
{splitResponseParts(value).map((part, index) => (
part.type === 'think' ? (
<details key={`${part.type}-${index}`} className="rounded-lg border border-amber-100 bg-amber-50/80 p-2">
<summary className="cursor-pointer select-none text-[10px] font-bold uppercase tracking-wider text-amber-700 hover:text-amber-800">
</summary>
<div className="mt-2 whitespace-pre-wrap break-all text-amber-800">{part.content}</div>
</details>
) : (
<div key={`${part.type}-${index}`} className="whitespace-pre-wrap break-all">{part.content}</div>
)
))}
</>
);
}
function StructuredValuePreview({ value, depth = 0 }: { value: unknown; depth?: number }) {
const parsedString = typeof value === 'string' ? parseJsonLikeString(value) : undefined;
const displayValue = parsedString ?? value;
if (typeof displayValue === 'string') {
return <ResponseText value={displayValue} />;
}
if (displayValue === null || typeof displayValue !== 'object') {
return <pre className="m-0 whitespace-pre-wrap break-all">{String(displayValue)}</pre>;
}
if (Array.isArray(displayValue)) {
if (!displayValue.length) {
return <span className="text-slate-400">[]</span>;
}
return (
<div className="space-y-2">
{displayValue.map((item, index) => (
<div key={index} className="rounded-lg border border-slate-100 bg-white/70 p-2">
<div className="mb-1 text-[9px] font-bold uppercase tracking-wider text-[#94a3b8]">Item {index + 1}</div>
<StructuredValuePreview value={item} depth={depth + 1} />
</div>
))}
</div>
);
}
const entries = Object.entries(displayValue);
if (!entries.length) {
return <span className="text-slate-400">{'{}'}</span>;
}
return (
<div className="space-y-2">
{entries.map(([key, fieldValue]) => {
const isNested = typeof fieldValue === 'object' && fieldValue !== null;
return (
<div key={key} className="rounded-lg border border-slate-100 bg-white/70 p-2">
<div className="mb-1 text-[9px] font-bold uppercase tracking-wider text-[#64748b]">{key}</div>
{isNested && depth > 0 ? (
<details>
<summary className="cursor-pointer select-none text-[10px] font-bold text-[#94a3b8] hover:text-[#0f172a]">
{Array.isArray(fieldValue) ? `${fieldValue.length} items` : 'Object'}
</summary>
<div className="mt-2">
<StructuredValuePreview value={fieldValue} depth={depth + 1} />
</div>
</details>
) : (
<StructuredValuePreview value={fieldValue} depth={depth + 1} />
)}
</div>
);
})}
</div>
);
}
function ResponsePreview({ value }: { value: unknown }) {
const normalizedValue = isRecord(value) && 'answer' in value && Object.keys(value).length === 1
? value.answer
: value;
// Resolve once at the top so we can pick chrome based on the *final* shape:
// a JSON-like string is treated as structured, not as a plain text answer.
const parsedFromString = typeof normalizedValue === 'string'
? parseJsonLikeString(normalizedValue)
: undefined;
const displayValue = parsedFromString ?? normalizedValue;
if (typeof displayValue === 'string') {
// Plain text answer — match the sibling cells (sans-serif, text-xs) so
// the row reads as one piece instead of a bolted-on console panel.
return (
<div className="max-h-40 overflow-auto space-y-2 text-xs leading-relaxed text-[#0f172a]">
<ResponseText value={displayValue} />
</div>
);
}
// Structured/JSON value — keep monospace, but lighter chrome that mirrors
// the <pre> blocks in the trace dialog (rounded-lg, no inset shadow).
return (
<div className="max-h-40 overflow-auto space-y-2 rounded-lg border border-slate-100 bg-slate-50/60 p-3 font-mono text-[10px] leading-relaxed text-[#334155]">
<StructuredValuePreview value={displayValue} />
</div>
);
}
// Excel cap is 32,767 chars per cell; leave headroom for safety.
const EXCEL_CELL_LIMIT = 32000;
const TRACE_INLINE_JSON_LIMIT = 300;

5462
慧帮忙-主入口.yml Normal file

File diff suppressed because it is too large Load Diff