mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-22 05:24:23 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d5e30c2e6 | |||
| a2cdbb621b | |||
| b92d54254b | |||
| d22fa82d7c |
@@ -39,6 +39,7 @@ bin/lbs index
|
|||||||
```
|
```
|
||||||
|
|
||||||
Use `bin/lbs env show` to inspect defaults and `bin/lbs env doctor` when diagnosing local environment readiness. Env output is redacted by default; do not work around that by printing raw secrets.
|
Use `bin/lbs env show` to inspect defaults and `bin/lbs env doctor` when diagnosing local environment readiness. Env output is redacted by default; do not work around that by printing raw secrets.
|
||||||
|
`bin/lbs` is a generated local wrapper. If it is missing on a fresh checkout, run `npm run bootstrap` from this directory first; `npm install` also regenerates it via `prepare`.
|
||||||
Use `bin/lbs fixture check` before fixture-heavy cases such as MCP, RAG, multimodal, or plugin smoke tests.
|
Use `bin/lbs fixture check` before fixture-heavy cases such as MCP, RAG, multimodal, or plugin smoke tests.
|
||||||
Use `bin/lbs case list --ready` for cases that have no missing machine inputs and no manual preconditions. Use `bin/lbs case list --machine-ready` when you want to keep `manual-check` candidates and confirm their preconditions yourself.
|
Use `bin/lbs case list --ready` for cases that have no missing machine inputs and no manual preconditions. Use `bin/lbs case list --machine-ready` when you want to keep `manual-check` candidates and confirm their preconditions yourself.
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -35,9 +35,13 @@ and LangBot's own Local Agent) working with the LangBot ecosystem.
|
|||||||
|
|
||||||
## The `lbs` CLI
|
## The `lbs` CLI
|
||||||
|
|
||||||
The testing assets ship with a small CLI (`bin/lbs`, Node ≥ 22.6):
|
The testing assets ship with a small CLI (`bin/lbs`, Node >= 22.6). The
|
||||||
|
`bin/lbs` wrapper is a generated local entrypoint; on a fresh checkout, run
|
||||||
|
`npm run bootstrap` once if it is missing. `npm install` also regenerates it via
|
||||||
|
the `prepare` script.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
npm run bootstrap # create bin/lbs if missing
|
||||||
bin/lbs validate # validate skills/cases/troubleshooting structure
|
bin/lbs validate # validate skills/cases/troubleshooting structure
|
||||||
bin/lbs index # regenerate skills.index.json
|
bin/lbs index # regenerate skills.index.json
|
||||||
bin/lbs env show # inspect resolved env defaults (redacted)
|
bin/lbs env show # inspect resolved env defaults (redacted)
|
||||||
|
|||||||
@@ -5,6 +5,13 @@
|
|||||||
"lbs": "./bin/lbs"
|
"lbs": "./bin/lbs"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"bootstrap": "node scripts/bootstrap-lbs.mjs",
|
||||||
|
"prepare": "node scripts/bootstrap-lbs.mjs",
|
||||||
|
"prevalidate": "node scripts/bootstrap-lbs.mjs",
|
||||||
|
"preindex": "node scripts/bootstrap-lbs.mjs",
|
||||||
|
"preindex:check": "node scripts/bootstrap-lbs.mjs",
|
||||||
|
"pretest": "node scripts/bootstrap-lbs.mjs",
|
||||||
|
"precheck": "node scripts/bootstrap-lbs.mjs",
|
||||||
"lbs": "node src/lbs.ts",
|
"lbs": "node src/lbs.ts",
|
||||||
"test": "node test/lbs-cli.test.ts",
|
"test": "node test/lbs-cli.test.ts",
|
||||||
"validate": "node src/lbs.ts validate",
|
"validate": "node src/lbs.ts validate",
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||||
|
import { dirname, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
const binDir = resolve(root, "bin");
|
||||||
|
const lbsPath = resolve(binDir, "lbs");
|
||||||
|
const wrapper = [
|
||||||
|
"#!/usr/bin/env bash",
|
||||||
|
"set -euo pipefail",
|
||||||
|
"",
|
||||||
|
'SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"',
|
||||||
|
'exec node "$SCRIPT_DIR/../src/lbs.ts" "$@"',
|
||||||
|
"",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
await mkdir(binDir, { recursive: true });
|
||||||
|
|
||||||
|
let current = "";
|
||||||
|
try {
|
||||||
|
current = await readFile(lbsPath, "utf8");
|
||||||
|
} catch {
|
||||||
|
// Missing wrapper is the normal first-run path.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current !== wrapper) {
|
||||||
|
await writeFile(lbsPath, wrapper, "utf8");
|
||||||
|
await chmod(lbsPath, 0o755);
|
||||||
|
}
|
||||||
+1
-1
@@ -61,7 +61,7 @@ export function repoRoot(start: string): string {
|
|||||||
// root of this assets tree). Check it first so that when the tree lives
|
// root of this assets tree). Check it first so that when the tree lives
|
||||||
// inside a larger repo (e.g. LangBot/skills/), we stop at the assets root
|
// inside a larger repo (e.g. LangBot/skills/), we stop at the assets root
|
||||||
// and not at the outer repo's .git/README.md.
|
// and not at the outer repo's .git/README.md.
|
||||||
if (existsSync(`${current}/skills.index.json`) && existsSync(`${current}/bin/lbs`)) {
|
if (existsSync(`${current}/skills.index.json`) && existsSync(`${current}/schemas/case.schema.json`)) {
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
if (existsSync(`${current}/.git`) && existsSync(`${current}/README.md`)) {
|
if (existsSync(`${current}/.git`) && existsSync(`${current}/README.md`)) {
|
||||||
|
|||||||
+83
-41
@@ -15,6 +15,7 @@ import { commandTroubleSearch } from "../src/commands/trouble.ts";
|
|||||||
import { commandValidate } from "../src/commands/validate.ts";
|
import { commandValidate } from "../src/commands/validate.ts";
|
||||||
import { commandIndex } from "../src/commands/skill.ts";
|
import { commandIndex } from "../src/commands/skill.ts";
|
||||||
import { loadEnv } from "../src/fs.ts";
|
import { loadEnv } from "../src/fs.ts";
|
||||||
|
import { repoRoot } from "../src/cli.ts";
|
||||||
import {
|
import {
|
||||||
classifyDebugChatResult,
|
classifyDebugChatResult,
|
||||||
findNewFailureSignal,
|
findNewFailureSignal,
|
||||||
@@ -23,6 +24,19 @@ import {
|
|||||||
|
|
||||||
const root = process.cwd();
|
const root = process.cwd();
|
||||||
|
|
||||||
|
test("repo root detects the skills tree before generated bin exists", () => {
|
||||||
|
const tmp = mkdtempSync(join(tmpdir(), "lbs-root-no-bin-"));
|
||||||
|
try {
|
||||||
|
mkdirSync(join(tmp, "schemas"), { recursive: true });
|
||||||
|
mkdirSync(join(tmp, "skills", "langbot-testing"), { recursive: true });
|
||||||
|
writeFileSync(join(tmp, "skills.index.json"), "{}");
|
||||||
|
writeFileSync(join(tmp, "schemas", "case.schema.json"), "{}");
|
||||||
|
assert.equal(repoRoot(join(tmp, "skills", "langbot-testing")), tmp);
|
||||||
|
} finally {
|
||||||
|
rmSync(tmp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function ctx(args: string[]): CommandContext {
|
function ctx(args: string[]): CommandContext {
|
||||||
return { root, args };
|
return { root, args };
|
||||||
}
|
}
|
||||||
@@ -75,6 +89,19 @@ function suiteResult(caseId: string, runId: string, status = "pass", evidence =
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function withEnv<T>(values: Record<string, string>, fn: () => T): T {
|
||||||
|
const previous = new Map(Object.keys(values).map((key) => [key, process.env[key]]));
|
||||||
|
try {
|
||||||
|
for (const [key, value] of Object.entries(values)) process.env[key] = value;
|
||||||
|
return fn();
|
||||||
|
} finally {
|
||||||
|
for (const [key, value] of previous) {
|
||||||
|
if (value === undefined) delete process.env[key];
|
||||||
|
else process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function captureAsync(fn: () => Promise<number>): Promise<{ code: number; output: string }> {
|
async function captureAsync(fn: () => Promise<number>): Promise<{ code: number; output: string }> {
|
||||||
const originalLog = console.log;
|
const originalLog = console.log;
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
@@ -1417,15 +1444,20 @@ test("generic pipeline readiness accepts either URL or name target", () => {
|
|||||||
const originalUrl = process.env.LANGBOT_PIPELINE_URL;
|
const originalUrl = process.env.LANGBOT_PIPELINE_URL;
|
||||||
const originalName = process.env.LANGBOT_PIPELINE_NAME;
|
const originalName = process.env.LANGBOT_PIPELINE_NAME;
|
||||||
try {
|
try {
|
||||||
process.env.LANGBOT_PIPELINE_URL = "http://127.0.0.1:3000/home/pipelines?id=only-url";
|
withEnv({
|
||||||
process.env.LANGBOT_PIPELINE_NAME = "";
|
LANGBOT_BROWSER_PROFILE: "/tmp/langbot-test-profile",
|
||||||
|
LANGBOT_CHROMIUM_EXECUTABLE: "/tmp/langbot-test-chromium",
|
||||||
|
}, () => {
|
||||||
|
process.env.LANGBOT_PIPELINE_URL = "http://127.0.0.1:3000/home/pipelines?id=only-url";
|
||||||
|
process.env.LANGBOT_PIPELINE_NAME = "";
|
||||||
|
|
||||||
const ready = capture(() => commandTestPlan(ctx(["test", "plan", "pipeline-debug-chat", "--json"])));
|
const ready = capture(() => commandTestPlan(ctx(["test", "plan", "pipeline-debug-chat", "--json"])));
|
||||||
assert.equal(ready.code, 0);
|
assert.equal(ready.code, 0);
|
||||||
const plan = JSON.parse(ready.output);
|
const plan = JSON.parse(ready.output);
|
||||||
assert.equal(plan.env_readiness.status, "ready");
|
assert.equal(plan.env_readiness.status, "ready");
|
||||||
assert.equal(plan.automation_readiness.status, "ready");
|
assert.equal(plan.automation_readiness.status, "ready");
|
||||||
assert.ok(plan.automation_readiness.required.includes("LANGBOT_PIPELINE_URL|LANGBOT_PIPELINE_NAME"));
|
assert.ok(plan.automation_readiness.required.includes("LANGBOT_PIPELINE_URL|LANGBOT_PIPELINE_NAME"));
|
||||||
|
});
|
||||||
|
|
||||||
process.env.LANGBOT_PIPELINE_URL = "";
|
process.env.LANGBOT_PIPELINE_URL = "";
|
||||||
process.env.LANGBOT_PIPELINE_NAME = "";
|
process.env.LANGBOT_PIPELINE_NAME = "";
|
||||||
@@ -2174,27 +2206,32 @@ test("local-agent effective prompt case has runnable automation defaults", () =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("local-agent basic case can setup the local-agent pipeline env", () => {
|
test("local-agent basic case can setup the local-agent pipeline env", () => {
|
||||||
const result = capture(() => commandTestRun(ctx([
|
withEnv({
|
||||||
"test",
|
LANGBOT_BROWSER_PROFILE: "/tmp/langbot-test-profile",
|
||||||
"run",
|
LANGBOT_CHROMIUM_EXECUTABLE: "/tmp/langbot-test-chromium",
|
||||||
"local-agent-basic-debug-chat",
|
}, () => {
|
||||||
"--dry-run",
|
const result = capture(() => commandTestRun(ctx([
|
||||||
"--json",
|
"test",
|
||||||
])));
|
"run",
|
||||||
assert.equal(result.code, 0);
|
"local-agent-basic-debug-chat",
|
||||||
const run = JSON.parse(result.output);
|
"--dry-run",
|
||||||
assert.deepEqual(run.setup_automation.map((item: { entry: string }) => item.entry), [
|
"--json",
|
||||||
"node:scripts/e2e/ensure-local-agent-pipeline.mjs --write-env",
|
])));
|
||||||
]);
|
assert.equal(result.code, 0);
|
||||||
|
const run = JSON.parse(result.output);
|
||||||
|
assert.deepEqual(run.setup_automation.map((item: { entry: string }) => item.entry), [
|
||||||
|
"node:scripts/e2e/ensure-local-agent-pipeline.mjs --write-env",
|
||||||
|
]);
|
||||||
|
|
||||||
const planResult = capture(() => commandTestPlan(ctx(["test", "plan", "local-agent-basic-debug-chat", "--json"])));
|
const planResult = capture(() => commandTestPlan(ctx(["test", "plan", "local-agent-basic-debug-chat", "--json"])));
|
||||||
assert.equal(planResult.code, 0);
|
assert.equal(planResult.code, 0);
|
||||||
const plan = JSON.parse(planResult.output);
|
const plan = JSON.parse(planResult.output);
|
||||||
assert.deepEqual(plan.setup_provides_env, [
|
assert.deepEqual(plan.setup_provides_env, [
|
||||||
"LANGBOT_LOCAL_AGENT_PIPELINE_URL",
|
"LANGBOT_LOCAL_AGENT_PIPELINE_URL",
|
||||||
"LANGBOT_LOCAL_AGENT_PIPELINE_NAME",
|
"LANGBOT_LOCAL_AGENT_PIPELINE_NAME",
|
||||||
]);
|
]);
|
||||||
assert.equal(plan.automation_readiness.status, "ready");
|
assert.equal(plan.automation_readiness.status, "ready");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("local-agent nonstreaming case disables stream output through automation defaults", () => {
|
test("local-agent nonstreaming case disables stream output through automation defaults", () => {
|
||||||
@@ -2355,20 +2392,25 @@ test("AgentRunner QA Debug Chat case uses dedicated pipeline env", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("AgentRunner QA Debug Chat setup automation removes manual readiness", () => {
|
test("AgentRunner QA Debug Chat setup automation removes manual readiness", () => {
|
||||||
const planResult = capture(() => commandTestPlan(ctx(["test", "plan", "agent-runner-qa-debug-chat", "--json"])));
|
withEnv({
|
||||||
assert.equal(planResult.code, 0);
|
LANGBOT_BROWSER_PROFILE: "/tmp/langbot-test-profile",
|
||||||
const plan = JSON.parse(planResult.output);
|
LANGBOT_CHROMIUM_EXECUTABLE: "/tmp/langbot-test-chromium",
|
||||||
assert.equal(plan.manual_readiness.status, "not_required");
|
}, () => {
|
||||||
assert.deepEqual(plan.setup_provides_env, [
|
const planResult = capture(() => commandTestPlan(ctx(["test", "plan", "agent-runner-qa-debug-chat", "--json"])));
|
||||||
"LANGBOT_QA_AGENT_RUNNER_PIPELINE_URL",
|
assert.equal(planResult.code, 0);
|
||||||
"LANGBOT_QA_AGENT_RUNNER_PIPELINE_NAME",
|
const plan = JSON.parse(planResult.output);
|
||||||
]);
|
assert.equal(plan.manual_readiness.status, "not_required");
|
||||||
assert.equal(plan.automation_readiness.status, "ready");
|
assert.deepEqual(plan.setup_provides_env, [
|
||||||
|
"LANGBOT_QA_AGENT_RUNNER_PIPELINE_URL",
|
||||||
|
"LANGBOT_QA_AGENT_RUNNER_PIPELINE_NAME",
|
||||||
|
]);
|
||||||
|
assert.equal(plan.automation_readiness.status, "ready");
|
||||||
|
|
||||||
const suiteResult = capture(() => commandSuitePlan(ctx(["suite", "plan", "agent-runner-release-gate", "--json"])));
|
const suiteResult = capture(() => commandSuitePlan(ctx(["suite", "plan", "agent-runner-release-gate", "--json"])));
|
||||||
assert.equal(suiteResult.code, 0);
|
assert.equal(suiteResult.code, 0);
|
||||||
const suite = JSON.parse(suiteResult.output);
|
const suite = JSON.parse(suiteResult.output);
|
||||||
assert.ok(!suite.readiness.manual_check_cases.includes("agent-runner-qa-debug-chat"));
|
assert.ok(!suite.readiness.manual_check_cases.includes("agent-runner-qa-debug-chat"));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ACP AgentRunner Debug Chat case setups the ACP pipeline env", () => {
|
test("ACP AgentRunner Debug Chat case setups the ACP pipeline env", () => {
|
||||||
|
|||||||
@@ -84,7 +84,13 @@ class CommandManager:
|
|||||||
|
|
||||||
privilege = 1
|
privilege = 1
|
||||||
|
|
||||||
if f'{query.launcher_type.value}_{query.launcher_id}' in self.ap.instance_config.data['admins']:
|
admins = self.ap.instance_config.data['admins']
|
||||||
|
launcher_session_id = f'{query.launcher_type.value}_{query.launcher_id}'
|
||||||
|
sender_session_id = f'person_{query.sender_id}'
|
||||||
|
|
||||||
|
# 兼容老版本匹配 launcher_session_id(群管理: group_xxx 私聊管理: person_xxx)
|
||||||
|
# 新实现匹配 sender_session_id(个人管理员: person_xxx,在任何群聊中生效)
|
||||||
|
if launcher_session_id in admins or sender_session_id in admins:
|
||||||
privilege = 2
|
privilege = 2
|
||||||
|
|
||||||
ctx = command_context.ExecuteContext(
|
ctx = command_context.ExecuteContext(
|
||||||
|
|||||||
@@ -23,7 +23,13 @@ class CommandHandler(handler.MessageHandler):
|
|||||||
|
|
||||||
privilege = 1
|
privilege = 1
|
||||||
|
|
||||||
if f'{query.launcher_type.value}_{query.launcher_id}' in self.ap.instance_config.data['admins']:
|
admins = self.ap.instance_config.data['admins']
|
||||||
|
launcher_session_id = f'{query.launcher_type.value}_{query.launcher_id}'
|
||||||
|
sender_session_id = f'person_{query.sender_id}'
|
||||||
|
|
||||||
|
# 兼容老版本匹配 launcher_session_id(群管理: group_xxx 私聊管理: person_xxx)
|
||||||
|
# 新实现匹配 sender_session_id(个人管理员: person_xxx,在任何群聊中生效)
|
||||||
|
if launcher_session_id in admins or sender_session_id in admins:
|
||||||
privilege = 2
|
privilege = 2
|
||||||
|
|
||||||
spt = command_text.split(' ')
|
spt = command_text.split(' ')
|
||||||
|
|||||||
@@ -646,7 +646,7 @@ export default function ModelsPanel({
|
|||||||
</PanelBody>
|
</PanelBody>
|
||||||
|
|
||||||
<Dialog open={providerFormOpen} onOpenChange={setProviderFormOpen}>
|
<Dialog open={providerFormOpen} onOpenChange={setProviderFormOpen}>
|
||||||
<DialogContent className="w-[600px] p-6">
|
<DialogContent className="w-full max-w-[calc(100%-2rem)] p-4 sm:max-w-[600px] sm:p-6">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{editingProviderId
|
{editingProviderId
|
||||||
|
|||||||
@@ -38,7 +38,12 @@ export function PanelBody({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={cn('min-h-0 flex-1 overflow-auto px-6 py-5', className)}>
|
<div
|
||||||
|
className={cn(
|
||||||
|
'min-h-0 flex-1 overflow-auto px-3 py-4 sm:px-6 sm:py-5',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -109,10 +109,10 @@ export default function MonitoringFilters({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center gap-6">
|
<div className="flex w-full flex-col gap-3 sm:w-auto sm:flex-row sm:flex-wrap sm:items-center sm:gap-6">
|
||||||
{/* Bot Filter */}
|
{/* Bot Filter */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<label className="text-sm font-medium text-foreground whitespace-nowrap">
|
<label className="w-20 shrink-0 text-sm font-medium text-foreground sm:w-auto sm:whitespace-nowrap">
|
||||||
{t('monitoring.filters.bot')}
|
{t('monitoring.filters.bot')}
|
||||||
</label>
|
</label>
|
||||||
<Select
|
<Select
|
||||||
@@ -120,7 +120,7 @@ export default function MonitoringFilters({
|
|||||||
onValueChange={handleBotChange}
|
onValueChange={handleBotChange}
|
||||||
disabled={loadingBots}
|
disabled={loadingBots}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-9 w-[140px]">
|
<SelectTrigger className="h-9 w-full sm:w-[140px]">
|
||||||
<SelectValue
|
<SelectValue
|
||||||
placeholder={
|
placeholder={
|
||||||
loadingBots
|
loadingBots
|
||||||
@@ -144,7 +144,7 @@ export default function MonitoringFilters({
|
|||||||
|
|
||||||
{/* Pipeline Filter */}
|
{/* Pipeline Filter */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<label className="text-sm font-medium text-foreground whitespace-nowrap">
|
<label className="w-20 shrink-0 text-sm font-medium text-foreground sm:w-auto sm:whitespace-nowrap">
|
||||||
{t('monitoring.filters.pipeline')}
|
{t('monitoring.filters.pipeline')}
|
||||||
</label>
|
</label>
|
||||||
<Select
|
<Select
|
||||||
@@ -152,7 +152,7 @@ export default function MonitoringFilters({
|
|||||||
onValueChange={handlePipelineChange}
|
onValueChange={handlePipelineChange}
|
||||||
disabled={loadingPipelines}
|
disabled={loadingPipelines}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-9 w-[140px]">
|
<SelectTrigger className="h-9 w-full sm:w-[140px]">
|
||||||
<SelectValue
|
<SelectValue
|
||||||
placeholder={
|
placeholder={
|
||||||
loadingPipelines
|
loadingPipelines
|
||||||
@@ -176,11 +176,11 @@ export default function MonitoringFilters({
|
|||||||
|
|
||||||
{/* Time Range Filter */}
|
{/* Time Range Filter */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<label className="text-sm font-medium text-foreground whitespace-nowrap">
|
<label className="w-20 shrink-0 text-sm font-medium text-foreground sm:w-auto sm:whitespace-nowrap">
|
||||||
{t('monitoring.filters.timeRange')}
|
{t('monitoring.filters.timeRange')}
|
||||||
</label>
|
</label>
|
||||||
<Select value={timeRange} onValueChange={handleTimeRangeChange}>
|
<Select value={timeRange} onValueChange={handleTimeRangeChange}>
|
||||||
<SelectTrigger className="h-9 w-[150px]">
|
<SelectTrigger className="h-9 w-full sm:w-[150px]">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ function MonitoringPageContent() {
|
|||||||
{/* Filters and Refresh Button - Sticky */}
|
{/* Filters and Refresh Button - Sticky */}
|
||||||
<div className="sticky top-0 z-10 -mt-1 pb-5 pt-1 bg-background">
|
<div className="sticky top-0 z-10 -mt-1 pb-5 pt-1 bg-background">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-wrap items-center justify-between gap-4 p-4 bg-card rounded-xl border">
|
<div className="flex flex-col gap-3 p-3 bg-card rounded-xl border sm:flex-row sm:flex-wrap sm:items-center sm:justify-between sm:gap-4 sm:p-4">
|
||||||
<MonitoringFilters
|
<MonitoringFilters
|
||||||
selectedBots={filterState.selectedBots}
|
selectedBots={filterState.selectedBots}
|
||||||
selectedPipelines={filterState.selectedPipelines}
|
selectedPipelines={filterState.selectedPipelines}
|
||||||
@@ -285,7 +285,7 @@ function MonitoringPageContent() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
className="shadow-sm flex-shrink-0"
|
className="flex-1 shadow-sm sm:flex-shrink-0 sm:flex-none"
|
||||||
>
|
>
|
||||||
<RefreshCw className="w-4 h-4 mr-2" />
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
{t('monitoring.refreshData')}
|
{t('monitoring.refreshData')}
|
||||||
@@ -312,27 +312,27 @@ function MonitoringPageContent() {
|
|||||||
onValueChange={setActiveTab}
|
onValueChange={setActiveTab}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
<div className="px-6 pt-4">
|
<div className="px-3 pt-4 sm:px-6">
|
||||||
<TabsList className="h-12 p-1">
|
<TabsList className="h-12 w-full justify-start gap-1 overflow-x-auto p-1 sm:w-auto">
|
||||||
<TabsTrigger value="messages" className="px-6 py-2">
|
<TabsTrigger value="messages" className="px-3 py-2 sm:px-6">
|
||||||
{t('monitoring.tabs.messages')}
|
{t('monitoring.tabs.messages')}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="modelCalls" className="px-6 py-2">
|
<TabsTrigger value="modelCalls" className="px-3 py-2 sm:px-6">
|
||||||
{t('monitoring.tabs.modelCalls')}
|
{t('monitoring.tabs.modelCalls')}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="tokens" className="px-6 py-2">
|
<TabsTrigger value="tokens" className="px-3 py-2 sm:px-6">
|
||||||
{t('monitoring.tabs.tokens')}
|
{t('monitoring.tabs.tokens')}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="feedback" className="px-6 py-2">
|
<TabsTrigger value="feedback" className="px-3 py-2 sm:px-6">
|
||||||
{t('monitoring.tabs.feedback')}
|
{t('monitoring.tabs.feedback')}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="errors" className="px-6 py-2">
|
<TabsTrigger value="errors" className="px-3 py-2 sm:px-6">
|
||||||
{t('monitoring.tabs.errors')}
|
{t('monitoring.tabs.errors')}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TabsContent value="messages" className="p-6 m-0">
|
<TabsContent value="messages" className="p-3 m-0 sm:p-6">
|
||||||
<div>
|
<div>
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="py-12 flex justify-center">
|
<div className="py-12 flex justify-center">
|
||||||
@@ -362,7 +362,7 @@ function MonitoringPageContent() {
|
|||||||
>
|
>
|
||||||
{/* Message Header - Always Visible */}
|
{/* Message Header - Always Visible */}
|
||||||
<div
|
<div
|
||||||
className="p-5 cursor-pointer hover:bg-accent transition-colors"
|
className="p-3 cursor-pointer hover:bg-accent transition-colors sm:p-5"
|
||||||
onClick={() => toggleMessageExpand(msg.id)}
|
onClick={() => toggleMessageExpand(msg.id)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
@@ -466,7 +466,7 @@ function MonitoringPageContent() {
|
|||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="modelCalls" className="p-6 m-0">
|
<TabsContent value="modelCalls" className="p-3 m-0 sm:p-6">
|
||||||
<div>
|
<div>
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="py-12 flex justify-center">
|
<div className="py-12 flex justify-center">
|
||||||
@@ -482,7 +482,7 @@ function MonitoringPageContent() {
|
|||||||
{data.modelCalls.map((call) => (
|
{data.modelCalls.map((call) => (
|
||||||
<div
|
<div
|
||||||
key={call.id}
|
key={call.id}
|
||||||
className="border rounded-xl p-5 transition-all duration-200"
|
className="border rounded-xl p-3 transition-all duration-200 sm:p-5"
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-start mb-3">
|
<div className="flex justify-between items-start mb-3">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@@ -672,7 +672,7 @@ function MonitoringPageContent() {
|
|||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="tokens" className="p-6 m-0">
|
<TabsContent value="tokens" className="p-3 m-0 sm:p-6">
|
||||||
<TokenMonitoring
|
<TokenMonitoring
|
||||||
botIds={
|
botIds={
|
||||||
filterState.selectedBots.length > 0
|
filterState.selectedBots.length > 0
|
||||||
@@ -690,7 +690,7 @@ function MonitoringPageContent() {
|
|||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="feedback" className="p-6 m-0">
|
<TabsContent value="feedback" className="p-3 m-0 sm:p-6">
|
||||||
<div>
|
<div>
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="py-12 flex justify-center">
|
<div className="py-12 flex justify-center">
|
||||||
@@ -722,7 +722,7 @@ function MonitoringPageContent() {
|
|||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="errors" className="p-6 m-0">
|
<TabsContent value="errors" className="p-3 m-0 sm:p-6">
|
||||||
<div>
|
<div>
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="py-12 flex justify-center">
|
<div className="py-12 flex justify-center">
|
||||||
@@ -739,7 +739,7 @@ function MonitoringPageContent() {
|
|||||||
>
|
>
|
||||||
{/* Error Header - Always Visible */}
|
{/* Error Header - Always Visible */}
|
||||||
<div
|
<div
|
||||||
className="p-5 cursor-pointer hover:bg-red-50 dark:hover:bg-red-950/50 transition-colors bg-red-50/50 dark:bg-red-950/30"
|
className="p-3 cursor-pointer hover:bg-red-50 dark:hover:bg-red-950/50 transition-colors bg-red-50/50 dark:bg-red-950/30 sm:p-5"
|
||||||
onClick={() => toggleErrorExpand(error.id)}
|
onClick={() => toggleErrorExpand(error.id)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export default function PluginLogs({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div className="flex shrink-0 flex-wrap items-center gap-2 px-6 pb-3">
|
<div className="flex shrink-0 flex-wrap items-center gap-2 px-1 pb-3 sm:px-6">
|
||||||
<Select value={level} onValueChange={setLevel}>
|
<Select value={level} onValueChange={setLevel}>
|
||||||
<SelectTrigger className="h-8 w-[130px]">
|
<SelectTrigger className="h-8 w-[130px]">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
@@ -132,7 +132,7 @@ export default function PluginLogs({
|
|||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
className="min-h-0 flex-1 overflow-auto bg-gray-50 px-6 py-3 font-mono text-xs leading-relaxed dark:bg-gray-900/40"
|
className="min-h-0 flex-1 overflow-auto bg-gray-50 px-3 py-3 font-mono text-xs leading-relaxed dark:bg-gray-900/40 sm:px-6"
|
||||||
>
|
>
|
||||||
{logs.length === 0 ? (
|
{logs.length === 0 ? (
|
||||||
<div className="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
|
<div className="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
|||||||
Reference in New Issue
Block a user