mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 00:31:19 +08:00
fix(knowledge-fs): harden research progress and scoring
This commit is contained in:
parent
f1045bc099
commit
480deebf02
@ -0,0 +1,69 @@
|
||||
# Research progress and structured semantic scoring hardening
|
||||
|
||||
Date: 2026-07-31
|
||||
|
||||
## What changed
|
||||
|
||||
- Routed PageIndex semantic candidate scoring through Dify's structured-output LLM endpoint.
|
||||
Each bounded scoring batch now supplies a strict JSON schema for the exact candidate ids, while
|
||||
the existing KnowledgeFS parser continues to reject missing, duplicated, unknown, or invalid
|
||||
scores. The production batch size was reduced from ten candidates to five.
|
||||
- Updated the Dify model-runtime client and generation provider to carry structured-output schemas,
|
||||
consume Dify's validated `structured_output` stream field, and retain free-form response text
|
||||
only as a compatibility fallback.
|
||||
- Kept Research task stage transitions synchronized with heartbeat-updated row versions inside the
|
||||
serialized lease lane, preventing a heartbeat from making the next durable transition use a
|
||||
stale fence.
|
||||
- Made the Research progress database repository accept PostgreSQL `BIGINT` timestamps returned as
|
||||
bounded decimal strings while preserving non-negative safe-integer validation.
|
||||
- Added Retrieval Test progress streaming through the console-issued Research capability, including
|
||||
bounded SSE replay, cursor reconnects, terminal task/partial refreshes, actual per-stage timing,
|
||||
and process-log presentation.
|
||||
- Split active Research stage labels from completed milestone labels in every supported Web locale.
|
||||
A streamed completion now refreshes the task first and leaves the existing active-to-completed
|
||||
transition guard as the single owner of the final partial-result refresh. Historical terminal
|
||||
replay does not trigger redundant task or partial requests.
|
||||
|
||||
## Why
|
||||
|
||||
- Free-form LLM output could spend tokens on prose or return malformed JSON before semantic scores,
|
||||
even though Dify already owns tenant-scoped structured-output invocation and validation.
|
||||
- Research heartbeats increment the durable row version while generation is running. Stage
|
||||
transitions must read the latest fence from the same serialized lane to avoid false lease loss.
|
||||
- PostgreSQL drivers commonly expose `BIGINT` values as strings, so valid persisted progress events
|
||||
could fail when read back.
|
||||
- Polling alone did not expose durable Research stage history or precise timing, while mixing SSE
|
||||
terminal refreshes with the existing completion guard caused duplicate partial reads.
|
||||
- Completed-milestone copy was misleading when reused for a stage that was still running.
|
||||
|
||||
## Verification
|
||||
|
||||
- Web Retrieval Test and Research event-stream focused suites pass.
|
||||
- KnowledgeFS API semantic scoring, progress database, and Research runtime focused suites pass.
|
||||
- Dify model-runtime client and generation-provider focused suites pass.
|
||||
- TypeScript checks pass for `@knowledge/api`, `@knowledge/dify-model-runtime-client`, and
|
||||
`@knowledge/generation`.
|
||||
- Focused KnowledgeFS Biome checks, Web i18n synchronization, and `git diff --check` pass.
|
||||
- The repository-wide Web TypeScript command remains blocked by pre-existing generated
|
||||
`.next/types/validator.ts` route declarations unrelated to this change.
|
||||
- Vite+ static `check` remains unavailable in the current local environment because its launcher
|
||||
cannot resolve the Node binary; focused tests, i18n validation, KnowledgeFS Biome, and package
|
||||
typechecks were used instead.
|
||||
|
||||
## Performance and operational bounds
|
||||
|
||||
- Semantic scoring still uses at most four concurrent LLM batches, with five candidates per batch,
|
||||
bounded candidate text, output tokens, response characters, and per-batch timeouts.
|
||||
- Research SSE responses remain server-bounded by page limit and connection duration. Clients
|
||||
reconnect from the last durable sequence cursor rather than retaining an unbounded connection.
|
||||
- Completed tasks perform one final partial refetch through the existing transition guard. Replayed
|
||||
historical terminal events do not add redundant refresh requests.
|
||||
|
||||
## Risks and follow-up
|
||||
|
||||
- Smaller semantic-score batches increase the number of model calls for the same candidate window,
|
||||
trading request count for more reliable structured responses within the configured output budget.
|
||||
- Providers without native structured output still rely on Dify's prompt-based parsing before the
|
||||
strict KnowledgeFS validation boundary.
|
||||
- Progress history is held only for Research tasks selected during the current Retrieval Test page
|
||||
session; switching or reloading reconstructs it from the durable cursor stream.
|
||||
@ -149,7 +149,7 @@ const rerankerOptions = createApiRerankerOptions();
|
||||
const semanticEntityExtractionOptions = createApiSemanticEntityExtractionOptions();
|
||||
const profileReasoningCapability = createApiProfileReasoningCapability();
|
||||
const pageIndexSemanticTreeSearch = createPageIndexSemanticTreeSearch({
|
||||
batchSize: 10,
|
||||
batchSize: 5,
|
||||
maxConcurrentBatches: 4,
|
||||
maxOutputTokens: profileReasoningCapability.maxOutputTokens,
|
||||
maxTextCharsPerCandidate: 1_500,
|
||||
|
||||
@ -65,6 +65,27 @@ describe("PageIndex semantic tree search", () => {
|
||||
}),
|
||||
);
|
||||
const firstPrompt = generate.mock.calls[0]?.[0].messages;
|
||||
expect(generate.mock.calls[0]?.[0].structuredOutputSchema).toMatchObject({
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
scores: {
|
||||
items: {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
candidateId: { enum: ["c1", "c2"], type: "string" },
|
||||
score: { maximum: 1, minimum: 0, type: "number" },
|
||||
},
|
||||
required: ["candidateId", "score", "reason"],
|
||||
type: "object",
|
||||
},
|
||||
maxItems: 2,
|
||||
minItems: 2,
|
||||
type: "array",
|
||||
},
|
||||
},
|
||||
required: ["scores"],
|
||||
type: "object",
|
||||
});
|
||||
expect(firstPrompt?.[0]?.content).toContain("do not score by keyword overlap alone");
|
||||
expect(firstPrompt?.[1]?.content).toContain("How long must invoices be retained?");
|
||||
expect(firstPrompt?.[1]?.content).toContain("Finance");
|
||||
|
||||
@ -44,6 +44,7 @@ export interface GeneratePageIndexSemanticScoreInput {
|
||||
}[];
|
||||
readonly model: string;
|
||||
readonly signal?: AbortSignal | undefined;
|
||||
readonly structuredOutputSchema?: Readonly<Record<string, unknown>> | undefined;
|
||||
readonly temperature?: number | undefined;
|
||||
readonly tenantId?: string | undefined;
|
||||
}
|
||||
@ -159,6 +160,7 @@ export function createPageIndexSemanticTreeSearch({
|
||||
}),
|
||||
model: input.reasoningModel.model,
|
||||
signal: controller.signal,
|
||||
structuredOutputSchema: semanticScoreOutputSchema(candidates),
|
||||
temperature: 0,
|
||||
tenantId,
|
||||
}),
|
||||
@ -228,6 +230,36 @@ function semanticScoreMessages({
|
||||
];
|
||||
}
|
||||
|
||||
function semanticScoreOutputSchema(
|
||||
candidates: readonly PageIndexSemanticCandidate[],
|
||||
): Readonly<Record<string, unknown>> {
|
||||
return {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
scores: {
|
||||
items: {
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
candidateId: {
|
||||
enum: candidates.map((candidate) => candidate.candidateId),
|
||||
type: "string",
|
||||
},
|
||||
reason: { maxLength: 500, minLength: 1, type: "string" },
|
||||
score: { maximum: 1, minimum: 0, type: "number" },
|
||||
},
|
||||
required: ["candidateId", "score", "reason"],
|
||||
type: "object",
|
||||
},
|
||||
maxItems: candidates.length,
|
||||
minItems: candidates.length,
|
||||
type: "array",
|
||||
},
|
||||
},
|
||||
required: ["scores"],
|
||||
type: "object",
|
||||
};
|
||||
}
|
||||
|
||||
function parseSemanticScoreOutput(
|
||||
text: string,
|
||||
candidates: readonly PageIndexSemanticCandidate[],
|
||||
|
||||
@ -131,6 +131,23 @@ it("pages by tenant/task cursor and rejects unbounded reads", async () => {
|
||||
).rejects.toThrow(/between 1 and 1/u);
|
||||
});
|
||||
|
||||
it("reads PostgreSQL bigint timestamps returned as decimal strings", async () => {
|
||||
const database = recordingDatabase("postgres", async () => ({
|
||||
rows: [progressRow({ created_at: String(Date.parse("2026-07-14T00:00:00.000Z")) })],
|
||||
rowsAffected: 0,
|
||||
}));
|
||||
|
||||
await expect(
|
||||
createRepository(database.adapter).list({
|
||||
limit: 1,
|
||||
researchTaskJobId: JOB_ID,
|
||||
tenantId: TENANT_ID,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
items: [{ createdAt: "2026-07-14T00:00:00.000Z" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("polls the durable ledger independently across replicas and releases subscriber bounds", async () => {
|
||||
const rows: DatabaseRow[] = [];
|
||||
const database = recordingDatabase("postgres", async (input) => {
|
||||
|
||||
@ -9,7 +9,7 @@ import type {
|
||||
} from "@knowledge/core";
|
||||
import { UuidSchema } from "@knowledge/core";
|
||||
|
||||
import { numberColumn, stringColumn } from "./database-row-utils";
|
||||
import { nonnegativeSafeIntegerColumn, numberColumn, stringColumn } from "./database-row-utils";
|
||||
import { databasePlaceholder, quoteDatabaseIdentifier } from "./database-sql-utils";
|
||||
import { cloneJsonObject, jsonObjectColumn } from "./json-utils";
|
||||
import type { ResearchTaskJobStage } from "./research-task-job";
|
||||
@ -397,7 +397,7 @@ function progressFromRow(row: DatabaseRow): ResearchTaskProgressEvent {
|
||||
if (!eventTypes.has(type) || !jobStages.has(stage)) {
|
||||
throw new Error("Research task progress row has an invalid event type or stage");
|
||||
}
|
||||
const createdAt = validTimestamp(numberColumn(row, "created_at"));
|
||||
const createdAt = validTimestamp(nonnegativeSafeIntegerColumn(row, "created_at"));
|
||||
return {
|
||||
createdAt: new Date(createdAt).toISOString(),
|
||||
id: stringColumn(row, "id"),
|
||||
|
||||
@ -344,6 +344,99 @@ describe("research task production runtime", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("advances with the latest row version after a heartbeat during generation", async () => {
|
||||
const repository = new MemoryDurableRepository(baseJob());
|
||||
let heartbeatObserved: (() => void) | undefined;
|
||||
const heartbeat = new Promise<void>((resolve) => {
|
||||
heartbeatObserved = resolve;
|
||||
});
|
||||
const originalHeartbeat = repository.heartbeatExecution.bind(repository);
|
||||
const heartbeatExecution = vi
|
||||
.spyOn(repository, "heartbeatExecution")
|
||||
.mockImplementation(async (input) => {
|
||||
const updated = await originalHeartbeat(input);
|
||||
heartbeatObserved?.();
|
||||
return updated;
|
||||
});
|
||||
const runtime = createResearchTaskRuntime({
|
||||
...runtimeOptions(repository),
|
||||
generator: {
|
||||
stream: async function* () {
|
||||
await heartbeat;
|
||||
yield traceStep("query.retrieve");
|
||||
yield traceStep("query.answer");
|
||||
},
|
||||
},
|
||||
heartbeatIntervalMs: 1,
|
||||
leaseMs: 1_000,
|
||||
now: () => 1_000,
|
||||
});
|
||||
|
||||
await expect(runtime.tick()).resolves.toMatchObject({
|
||||
leased: 1,
|
||||
succeeded: 1,
|
||||
});
|
||||
expect(heartbeatExecution).toHaveBeenCalled();
|
||||
expect(repository.job.stage).toBe("completed");
|
||||
});
|
||||
|
||||
it("refreshes the stage-transition fence inside the serialized heartbeat lane", async () => {
|
||||
const repository = new MemoryDurableRepository(baseJob());
|
||||
let releaseStageGuard: (() => void) | undefined;
|
||||
const stageGuardRelease = new Promise<void>((resolve) => {
|
||||
releaseStageGuard = resolve;
|
||||
});
|
||||
let stageGuardObserved: (() => void) | undefined;
|
||||
const stageGuard = new Promise<void>((resolve) => {
|
||||
stageGuardObserved = resolve;
|
||||
});
|
||||
let heartbeatObserved: (() => void) | undefined;
|
||||
const heartbeat = new Promise<void>((resolve) => {
|
||||
heartbeatObserved = resolve;
|
||||
});
|
||||
let assertionCount = 0;
|
||||
const originalHeartbeat = repository.heartbeatExecution.bind(repository);
|
||||
vi.spyOn(repository, "heartbeatExecution").mockImplementation(async (input) => {
|
||||
const updated = await originalHeartbeat(input);
|
||||
heartbeatObserved?.();
|
||||
return updated;
|
||||
});
|
||||
const runtime = createResearchTaskRuntime({
|
||||
...runtimeOptions(repository),
|
||||
deletionFence: {
|
||||
assertDeletionFenceUnchanged: async () => {
|
||||
assertionCount += 1;
|
||||
if (assertionCount === 2) {
|
||||
stageGuardObserved?.();
|
||||
await stageGuardRelease;
|
||||
}
|
||||
},
|
||||
captureDeletionFence: async (scope) => ({ scope }) as never,
|
||||
},
|
||||
generator: {
|
||||
stream: async function* () {
|
||||
yield traceStep("query.retrieve");
|
||||
yield traceStep("query.answer");
|
||||
},
|
||||
},
|
||||
heartbeatIntervalMs: 1,
|
||||
leaseMs: 1_000,
|
||||
now: () => 1_000,
|
||||
});
|
||||
|
||||
const tick = runtime.tick();
|
||||
await stageGuard;
|
||||
await heartbeat;
|
||||
releaseStageGuard?.();
|
||||
|
||||
await expect(tick).resolves.toMatchObject({
|
||||
leased: 1,
|
||||
retryScheduled: 0,
|
||||
succeeded: 1,
|
||||
});
|
||||
expect(repository.job.stage).toBe("completed");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
error: `Research task partial result answer exceeds maxChars=${RESEARCH_TASK_PARTIAL_ANSWER_MAX_CHARS}`,
|
||||
@ -1210,8 +1303,23 @@ class MemoryDurableRepository implements ResearchTaskDurableRepository {
|
||||
return [structuredClone(this.job)];
|
||||
}
|
||||
|
||||
async heartbeatExecution(): Promise<ResearchTaskJob | null> {
|
||||
throw new Error("Unexpected heartbeat in a sub-interval test");
|
||||
async heartbeatExecution(
|
||||
input: ResearchTaskExecutionFence & {
|
||||
readonly leaseExpiresAt: number;
|
||||
readonly workerId: string;
|
||||
},
|
||||
): Promise<ResearchTaskJob | null> {
|
||||
if (!this.matchesFence(input) || input.workerId !== this.job.workerId) {
|
||||
return null;
|
||||
}
|
||||
this.job = {
|
||||
...this.job,
|
||||
heartbeatAt: input.now,
|
||||
leaseExpiresAt: input.leaseExpiresAt,
|
||||
rowVersion: this.job.rowVersion + 1,
|
||||
updatedAt: input.now,
|
||||
};
|
||||
return structuredClone(this.job);
|
||||
}
|
||||
|
||||
async advanceExecution(
|
||||
|
||||
@ -266,10 +266,10 @@ export function createResearchTaskRuntime({
|
||||
authorizationContext,
|
||||
abortSignal: abortController.signal,
|
||||
capabilityGrants,
|
||||
current,
|
||||
deletionFence,
|
||||
deletionToken,
|
||||
generator,
|
||||
getCurrent: () => current,
|
||||
manifests,
|
||||
now,
|
||||
partials,
|
||||
@ -277,6 +277,9 @@ export function createResearchTaskRuntime({
|
||||
publishProgress,
|
||||
repository,
|
||||
serialize,
|
||||
updateCurrent: (updated) => {
|
||||
current = updated;
|
||||
},
|
||||
});
|
||||
await assertWritable();
|
||||
const completed = await serialize(() => repository.completeExecution(fence(current, now())));
|
||||
@ -481,10 +484,10 @@ async function runResearchTask({
|
||||
authorizationContext: initialAuthorizationContext,
|
||||
abortSignal,
|
||||
capabilityGrants,
|
||||
current: initial,
|
||||
deletionFence,
|
||||
deletionToken,
|
||||
generator,
|
||||
getCurrent,
|
||||
manifests,
|
||||
now,
|
||||
partials,
|
||||
@ -492,6 +495,7 @@ async function runResearchTask({
|
||||
publishProgress,
|
||||
repository,
|
||||
serialize,
|
||||
updateCurrent,
|
||||
}: {
|
||||
readonly access: Pick<KnowledgeSpaceAccessService, "revalidatePermissionSnapshot">;
|
||||
readonly allowLegacyProfileFallback: boolean;
|
||||
@ -500,10 +504,10 @@ async function runResearchTask({
|
||||
readonly capabilityGrants?:
|
||||
| Pick<CapabilityGrantProvenanceRepository, "assertPublicationAllowed" | "get">
|
||||
| undefined;
|
||||
readonly current: ResearchTaskJob;
|
||||
readonly deletionFence?: DeletionLifecycleFenceGuard | undefined;
|
||||
readonly deletionToken?: DeletionLifecycleFenceToken | undefined;
|
||||
readonly generator: QueryGenerator;
|
||||
readonly getCurrent: () => ResearchTaskJob;
|
||||
readonly manifests: KnowledgeSpaceManifestRepository;
|
||||
readonly now: () => number;
|
||||
readonly partials: ResearchTaskPartialResultRepository;
|
||||
@ -515,8 +519,9 @@ async function runResearchTask({
|
||||
) => Promise<void>;
|
||||
readonly repository: ResearchTaskDurableRepository;
|
||||
readonly serialize: <T>(operation: () => Promise<T>) => Promise<T>;
|
||||
readonly updateCurrent: (current: ResearchTaskJob) => void;
|
||||
}): Promise<ResearchTaskJob> {
|
||||
let current = initial;
|
||||
let current = getCurrent();
|
||||
let authorizationContext = initialAuthorizationContext;
|
||||
const assertWritable = async (): Promise<void> => {
|
||||
if (deletionToken) {
|
||||
@ -524,6 +529,7 @@ async function runResearchTask({
|
||||
}
|
||||
};
|
||||
const revalidate = async () => {
|
||||
current = getCurrent();
|
||||
if (abortSignal.aborted) {
|
||||
throw abortSignal.reason ?? new Error("Research task execution lease was lost");
|
||||
}
|
||||
@ -534,15 +540,19 @@ async function runResearchTask({
|
||||
);
|
||||
};
|
||||
const advance = async (nextStage: ResearchTaskJobStage) => {
|
||||
const previousStage = current.stage;
|
||||
await assertWritable();
|
||||
const updated = await serialize(() =>
|
||||
repository.advanceExecution({ ...fence(current, now()), nextStage }),
|
||||
);
|
||||
const transition = await serialize(async () => {
|
||||
current = getCurrent();
|
||||
const previousStage = current.stage;
|
||||
const updated = await repository.advanceExecution({ ...fence(current, now()), nextStage });
|
||||
return { previousStage, updated };
|
||||
});
|
||||
const { previousStage, updated } = transition;
|
||||
if (!updated) {
|
||||
throw new Error("Research task stage transition lost its lease fence");
|
||||
}
|
||||
current = updated;
|
||||
updateCurrent(updated);
|
||||
await assertWritable();
|
||||
await publishProgress(updated, "research_task.stage_changed", { previousStage });
|
||||
};
|
||||
@ -684,7 +694,7 @@ async function runResearchTask({
|
||||
tenantId: current.tenantId,
|
||||
});
|
||||
}
|
||||
return current;
|
||||
return getCurrent();
|
||||
}
|
||||
|
||||
function durableRequestedMode(
|
||||
|
||||
@ -140,6 +140,63 @@ describe("createDifyModelRuntimeClient", () => {
|
||||
expect(fetchImpl).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("routes structured LLM requests through Dify's structured-output endpoint", async () => {
|
||||
let body: Record<string, unknown> | undefined;
|
||||
let url: string | undefined;
|
||||
const responseBody = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(
|
||||
frame({
|
||||
data: {
|
||||
delta: { finish_reason: "stop" },
|
||||
structured_output: { scores: [{ candidateId: "c1", score: 1 }] },
|
||||
},
|
||||
error: "",
|
||||
}),
|
||||
);
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
const client = createDifyModelRuntimeClient({
|
||||
apiKey: "inner-secret",
|
||||
baseUrl: "http://api:5001",
|
||||
fetch: vi.fn(async (input, init) => {
|
||||
body = JSON.parse(String(init?.body)) as Record<string, unknown>;
|
||||
url = String(input);
|
||||
return new Response(responseBody, {
|
||||
headers: { "content-type": "text/event-stream" },
|
||||
status: 200,
|
||||
});
|
||||
}),
|
||||
});
|
||||
const structuredOutputSchema = {
|
||||
properties: { scores: { type: "array" } },
|
||||
required: ["scores"],
|
||||
type: "object",
|
||||
};
|
||||
|
||||
const chunks: unknown[] = [];
|
||||
for await (const chunk of client.invokeLlm({
|
||||
model: "openrouter/auto",
|
||||
pluginId: "langgenius/openrouter",
|
||||
promptMessages: [{ content: "score", role: "user" }],
|
||||
provider: "openrouter",
|
||||
structuredOutputSchema,
|
||||
tenantId: "tenant-1",
|
||||
})) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
expect(url).toBe("http://api:5001/inner/api/invoke/llm/structured-output");
|
||||
expect(body).toMatchObject({ structured_output_schema: structuredOutputSchema });
|
||||
expect(chunks).toEqual([
|
||||
{
|
||||
delta: { finish_reason: "stop" },
|
||||
structured_output: { scores: [{ candidateId: "c1", score: 1 }] },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("lists tenant-active models through the Dify catalog endpoint", async () => {
|
||||
let body: unknown;
|
||||
const item = {
|
||||
|
||||
@ -50,6 +50,7 @@ export interface DifyLlmInput extends DifyModelRequestContext {
|
||||
readonly completionParams?: Readonly<Record<string, unknown>> | undefined;
|
||||
readonly promptMessages: readonly unknown[];
|
||||
readonly stop?: readonly string[] | undefined;
|
||||
readonly structuredOutputSchema?: Readonly<Record<string, unknown>> | undefined;
|
||||
readonly tools?: readonly unknown[] | undefined;
|
||||
}
|
||||
|
||||
@ -209,9 +210,12 @@ export function createDifyModelRuntimeClient(
|
||||
async *invokeLlm(input) {
|
||||
const deadline = createDeadline(input.signal, requestTimeoutMs);
|
||||
try {
|
||||
const structuredOutputSchema = input.structuredOutputSchema;
|
||||
const response = await request(
|
||||
fetchImpl,
|
||||
`${baseUrl}/inner/api/invoke/llm`,
|
||||
`${baseUrl}/inner/api/invoke/llm${
|
||||
structuredOutputSchema === undefined ? "" : "/structured-output"
|
||||
}`,
|
||||
apiKey,
|
||||
{
|
||||
...commonPayload(input),
|
||||
@ -221,6 +225,9 @@ export function createDifyModelRuntimeClient(
|
||||
prompt_messages: [...input.promptMessages],
|
||||
stop: [...(input.stop ?? [])],
|
||||
stream: true,
|
||||
...(structuredOutputSchema === undefined
|
||||
? {}
|
||||
: { structured_output_schema: { ...structuredOutputSchema } }),
|
||||
tools: [...(input.tools ?? [])],
|
||||
},
|
||||
deadline.signal,
|
||||
|
||||
@ -119,6 +119,42 @@ describe("Dify model runtime LLM provider", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers Dify's validated structured output over free-form response text", async () => {
|
||||
let captured: DifyLlmInput | undefined;
|
||||
const provider = createDifyModelRuntimeLlmProvider({
|
||||
...BASE,
|
||||
client: fakeClient(
|
||||
() => [
|
||||
deltaChunk("I will explain before returning JSON."),
|
||||
{
|
||||
delta: { finish_reason: "stop" },
|
||||
structured_output: {
|
||||
scores: [{ candidateId: "c1", reason: "direct", score: 1 }],
|
||||
},
|
||||
},
|
||||
],
|
||||
(input) => {
|
||||
captured = input;
|
||||
},
|
||||
),
|
||||
});
|
||||
const structuredOutputSchema = {
|
||||
properties: { scores: { type: "array" } },
|
||||
required: ["scores"],
|
||||
type: "object",
|
||||
};
|
||||
|
||||
const result = await provider.generate({
|
||||
messages: [{ content: "Score every candidate.", role: "user" }],
|
||||
model: BASE.model,
|
||||
structuredOutputSchema,
|
||||
tenantId: "tenant-abc",
|
||||
});
|
||||
|
||||
expect(captured?.structuredOutputSchema).toEqual(structuredOutputSchema);
|
||||
expect(result.text).toBe('{"scores":[{"candidateId":"c1","reason":"direct","score":1}]}');
|
||||
});
|
||||
|
||||
it("keeps content from Dify stream frames with null usage", async () => {
|
||||
const provider = createDifyModelRuntimeLlmProvider({
|
||||
...BASE,
|
||||
|
||||
@ -27,6 +27,7 @@ export interface GenerateTextInput {
|
||||
readonly messages: readonly LlmMessage[];
|
||||
readonly model: string;
|
||||
readonly signal?: AbortSignal;
|
||||
readonly structuredOutputSchema?: Readonly<Record<string, unknown>>;
|
||||
readonly temperature?: number;
|
||||
/** Tenant scope for Dify-managed model routing. */
|
||||
readonly tenantId?: string;
|
||||
@ -750,6 +751,7 @@ const DifyModelRuntimeLlmChunkSchema = z.object({
|
||||
.partial()
|
||||
.optional(),
|
||||
model: z.string().optional(),
|
||||
structured_output: z.unknown().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
@ -792,6 +794,8 @@ export function createDifyModelRuntimeLlmProvider(
|
||||
|
||||
const maxTokens = input.maxOutputTokens ?? options.maxOutputTokens;
|
||||
let finishReason = "stop";
|
||||
let structuredOutput: unknown;
|
||||
let structuredOutputFallbackText = "";
|
||||
let usage: LlmUsage | undefined;
|
||||
|
||||
for await (const chunk of options.client.invokeLlm({
|
||||
@ -806,6 +810,9 @@ export function createDifyModelRuntimeLlmProvider(
|
||||
role: message.role,
|
||||
})),
|
||||
provider: options.provider,
|
||||
...(input.structuredOutputSchema === undefined
|
||||
? {}
|
||||
: { structuredOutputSchema: input.structuredOutputSchema }),
|
||||
tenantId,
|
||||
...(input.signal ? { signal: input.signal } : {}),
|
||||
})) {
|
||||
@ -818,7 +825,15 @@ export function createDifyModelRuntimeLlmProvider(
|
||||
const content = parsed.data.delta?.message?.content;
|
||||
|
||||
if (content) {
|
||||
yield { delta: content, type: "delta" };
|
||||
if (input.structuredOutputSchema === undefined) {
|
||||
yield { delta: content, type: "delta" };
|
||||
} else {
|
||||
structuredOutputFallbackText += content;
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed.data.structured_output !== undefined) {
|
||||
structuredOutput = parsed.data.structured_output;
|
||||
}
|
||||
|
||||
const chunkUsage = parsed.data.delta?.usage;
|
||||
@ -842,6 +857,16 @@ export function createDifyModelRuntimeLlmProvider(
|
||||
}
|
||||
}
|
||||
|
||||
if (input.structuredOutputSchema !== undefined) {
|
||||
const text =
|
||||
structuredOutput === undefined
|
||||
? structuredOutputFallbackText
|
||||
: JSON.stringify(structuredOutput);
|
||||
if (text) {
|
||||
yield { delta: text, type: "delta" };
|
||||
}
|
||||
}
|
||||
|
||||
yield {
|
||||
finishReason,
|
||||
// Dify invokes the exact tenant-scoped catalog route requested above. Individual
|
||||
|
||||
114
web/features/new-rag/__tests__/research-task-events.spec.ts
Normal file
114
web/features/new-rag/__tests__/research-task-events.spec.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import type { KnowledgeFsStreamCapabilityResponse } from '@dify/contracts/api/console/knowledge-fs/types.gen'
|
||||
import { streamResearchTaskEvents } from '../services/research-task-events'
|
||||
|
||||
describe('streamResearchTaskEvents', () => {
|
||||
it('gets the capability stream and validates progress events', async () => {
|
||||
const encoder = new TextEncoder()
|
||||
const responseBody = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
'id: 1\nevent: research_task.progress\ndata: {"createdAt":"2026-07-31T10:00:00.000Z","id":"event-1","payload":{},"researchTaskJobId":"task-1","sequence":1,"stage":"planning","type":"research_task.started"}\n\n',
|
||||
),
|
||||
)
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
'id: 2\nevent: completed\ndata: {"createdAt":"2026-07-31T10:00:12.000Z","id":"event-2","payload":{},"researchTaskJobId":"task-1","sequence":2,"stage":"completed","type":"research_task.stage_changed"}\n\n',
|
||||
),
|
||||
)
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
const fetchMock = vi
|
||||
.spyOn(globalThis, 'fetch')
|
||||
.mockResolvedValue(new Response(responseBody, { status: 200 }))
|
||||
const events: unknown[] = []
|
||||
const capability: KnowledgeFsStreamCapabilityResponse = {
|
||||
expires_at: '2026-07-31T10:05:00.000Z',
|
||||
operation_id: 'streamResearchTask',
|
||||
token: `header.${btoa(JSON.stringify({ trace_id: 'research-trace-1' }))}.signature`,
|
||||
url: 'https://knowledge.example.test/research/events',
|
||||
}
|
||||
|
||||
const result = await streamResearchTaskEvents({
|
||||
capability,
|
||||
onEvent: (event) => events.push(event),
|
||||
})
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
capability.url,
|
||||
expect.objectContaining({
|
||||
credentials: 'omit',
|
||||
headers: expect.objectContaining({
|
||||
Accept: 'text/event-stream',
|
||||
Authorization: `Bearer ${capability.token}`,
|
||||
'X-Trace-ID': 'research-trace-1',
|
||||
}),
|
||||
method: 'GET',
|
||||
}),
|
||||
)
|
||||
expect(events).toEqual([
|
||||
{
|
||||
createdAt: '2026-07-31T10:00:00.000Z',
|
||||
id: 'event-1',
|
||||
payload: {},
|
||||
researchTaskJobId: 'task-1',
|
||||
sequence: 1,
|
||||
stage: 'planning',
|
||||
type: 'research_task.started',
|
||||
},
|
||||
{
|
||||
createdAt: '2026-07-31T10:00:12.000Z',
|
||||
id: 'event-2',
|
||||
payload: {},
|
||||
researchTaskJobId: 'task-1',
|
||||
sequence: 2,
|
||||
stage: 'completed',
|
||||
type: 'research_task.stage_changed',
|
||||
},
|
||||
])
|
||||
expect(result).toEqual({ cursor: '2', reconnect: false, terminal: true })
|
||||
|
||||
fetchMock.mockRestore()
|
||||
})
|
||||
|
||||
it('resumes from the latest cursor after a timeout', async () => {
|
||||
const encoder = new TextEncoder()
|
||||
const responseBody = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
'id: 3\nevent: research_task.progress\ndata: {"createdAt":"2026-07-31T10:00:20.000Z","id":"event-3","payload":{},"researchTaskJobId":"task-1","sequence":3,"stage":"retrieving","type":"research_task.stage_changed"}\n\n',
|
||||
),
|
||||
)
|
||||
controller.enqueue(
|
||||
encoder.encode('event: timeout\ndata: {"cursor":"3","researchTaskJobId":"task-1"}\n\n'),
|
||||
)
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
const fetchMock = vi
|
||||
.spyOn(globalThis, 'fetch')
|
||||
.mockResolvedValue(new Response(responseBody, { status: 200 }))
|
||||
const capability: KnowledgeFsStreamCapabilityResponse = {
|
||||
expires_at: '2026-07-31T10:05:00.000Z',
|
||||
operation_id: 'streamResearchTask',
|
||||
token: `header.${btoa(JSON.stringify({ trace_id: 'research-trace-1' }))}.signature`,
|
||||
url: 'https://knowledge.example.test/research/events?knowledgeSpaceId=space-1',
|
||||
}
|
||||
|
||||
const result = await streamResearchTaskEvents({
|
||||
capability,
|
||||
cursor: '2',
|
||||
onEvent: vi.fn(),
|
||||
})
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
`${capability.url}&cursor=2`,
|
||||
expect.objectContaining({ method: 'GET' }),
|
||||
)
|
||||
expect(result).toEqual({ cursor: '3', reconnect: true, terminal: false })
|
||||
|
||||
fetchMock.mockRestore()
|
||||
})
|
||||
})
|
||||
@ -15,7 +15,10 @@ const apiMock = vi.hoisted(() => ({
|
||||
refetchPartials: vi.fn(),
|
||||
refetchTasks: vi.fn(),
|
||||
refetchTraces: vi.fn(),
|
||||
researchTasks: [] as Array<Record<string, unknown>>,
|
||||
streamCapability: vi.fn(),
|
||||
streamQuery: vi.fn(),
|
||||
streamResearchEvents: vi.fn(),
|
||||
documentReferences: {} as Record<string, { id: string; title: string }>,
|
||||
evidence: undefined as Record<string, unknown> | undefined,
|
||||
traceDetail: undefined as Record<string, unknown> | undefined,
|
||||
@ -36,6 +39,10 @@ vi.mock('../services/knowledge-query-events', () => ({
|
||||
streamKnowledgeQuery: apiMock.streamQuery,
|
||||
}))
|
||||
|
||||
vi.mock('../services/research-task-events', () => ({
|
||||
streamResearchTaskEvents: apiMock.streamResearchEvents,
|
||||
}))
|
||||
|
||||
vi.mock('@tanstack/react-query', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('@tanstack/react-query')>()
|
||||
return {
|
||||
@ -65,7 +72,7 @@ vi.mock('@tanstack/react-query', async (importOriginal) => {
|
||||
}
|
||||
if (resource === 'tasks')
|
||||
return {
|
||||
data: { data: [] },
|
||||
data: { data: apiMock.researchTasks },
|
||||
refetch: apiMock.refetchTasks,
|
||||
}
|
||||
if (resource === 'partials')
|
||||
@ -81,6 +88,11 @@ vi.mock('@tanstack/react-query', async (importOriginal) => {
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleClient: {
|
||||
knowledgeFs: {
|
||||
tasks: {
|
||||
byTaskId: {
|
||||
streamCapability: { post: apiMock.streamCapability },
|
||||
},
|
||||
},
|
||||
spaces: {
|
||||
byControlSpaceId: {
|
||||
queries: { admission: { post: apiMock.queryAdmission } },
|
||||
@ -178,7 +190,18 @@ describe('RetrievalTestPage', () => {
|
||||
})
|
||||
apiMock.refetchTasks.mockResolvedValue(undefined)
|
||||
apiMock.refetchTraces.mockResolvedValue(undefined)
|
||||
apiMock.researchTasks = []
|
||||
apiMock.streamCapability.mockResolvedValue({
|
||||
expires_at: '2026-07-31T10:30:00.000Z',
|
||||
operation_id: 'streamResearchTask',
|
||||
token: 'capability-token',
|
||||
url: 'https://knowledge.example.test/research/events',
|
||||
})
|
||||
apiMock.streamQuery.mockResolvedValue(undefined)
|
||||
apiMock.streamResearchEvents.mockResolvedValue({
|
||||
reconnect: false,
|
||||
terminal: false,
|
||||
})
|
||||
apiMock.queryAdmission.mockResolvedValue({})
|
||||
apiMock.createBadCase.mockResolvedValue({ id: 'bad-case-1' })
|
||||
apiMock.createGolden.mockResolvedValue({ id: 'golden-1' })
|
||||
@ -228,6 +251,205 @@ describe('RetrievalTestPage', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('replays research progress events and shows actual stage durations', async () => {
|
||||
apiMock.researchTasks = [
|
||||
{
|
||||
completed_at: 1_800_000_025,
|
||||
cost: {},
|
||||
created_at: 1_800_000_000,
|
||||
id: 'research-completed',
|
||||
knowledge_space_id: 'space-1',
|
||||
metadata: {},
|
||||
mode: 'research',
|
||||
query: 'Compare the refund policies',
|
||||
stage: 'completed',
|
||||
updated_at: 1_800_000_025,
|
||||
},
|
||||
]
|
||||
apiMock.streamResearchEvents.mockImplementation(
|
||||
async ({
|
||||
onEvent,
|
||||
}: {
|
||||
onEvent: (event: {
|
||||
createdAt: string
|
||||
id: string
|
||||
payload: Record<string, unknown>
|
||||
researchTaskJobId: string
|
||||
sequence: number
|
||||
stage: string
|
||||
type: string
|
||||
}) => void
|
||||
}) => {
|
||||
const stages = [
|
||||
['planning', 0],
|
||||
['retrieving', 2],
|
||||
['analyzing', 7],
|
||||
['generating', 14],
|
||||
['completed', 25],
|
||||
] as const
|
||||
stages.forEach(([stage, seconds], index) =>
|
||||
onEvent({
|
||||
createdAt: new Date(1_800_000_000_000 + seconds * 1000).toISOString(),
|
||||
id: `event-${index + 1}`,
|
||||
payload: {},
|
||||
researchTaskJobId: 'research-completed',
|
||||
sequence: index + 1,
|
||||
stage,
|
||||
type: index ? 'research_task.stage_changed' : 'research_task.started',
|
||||
}),
|
||||
)
|
||||
return { cursor: '5', reconnect: false, terminal: true }
|
||||
},
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
renderPage()
|
||||
|
||||
await user.click(screen.getByText('Compare the refund policies'))
|
||||
expect(
|
||||
screen.getByRole('button', {
|
||||
name: 'dataset.newKnowledge.retrievalTest.processLog',
|
||||
}),
|
||||
).toHaveAttribute('aria-pressed', 'true')
|
||||
|
||||
await waitFor(() =>
|
||||
expect(apiMock.streamCapability).toHaveBeenCalledWith({
|
||||
body: { control_space_id: 'space-1' },
|
||||
params: { task_id: 'research-completed' },
|
||||
}),
|
||||
)
|
||||
expect(apiMock.streamResearchEvents).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
capability: expect.objectContaining({ operation_id: 'streamResearchTask' }),
|
||||
}),
|
||||
)
|
||||
expect(await screen.findByText('2s')).toBeInTheDocument()
|
||||
expect(screen.getByText('5s')).toBeInTheDocument()
|
||||
expect(screen.getByText('7s')).toBeInTheDocument()
|
||||
expect(screen.getByText('11s')).toBeInTheDocument()
|
||||
expect(apiMock.refetchTasks).not.toHaveBeenCalled()
|
||||
expect(apiMock.refetchPartials).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reconnects an active research event stream from its latest cursor', async () => {
|
||||
apiMock.researchTasks = [
|
||||
{
|
||||
cost: {},
|
||||
created_at: 1_800_000_000,
|
||||
id: 'research-active',
|
||||
knowledge_space_id: 'space-1',
|
||||
metadata: {},
|
||||
mode: 'research',
|
||||
query: 'Compare the refund policies',
|
||||
stage: 'retrieving',
|
||||
updated_at: 1_800_000_005,
|
||||
},
|
||||
]
|
||||
apiMock.streamResearchEvents
|
||||
.mockResolvedValueOnce({ cursor: '3', reconnect: true, terminal: false })
|
||||
.mockResolvedValueOnce({ cursor: '4', reconnect: false, terminal: true })
|
||||
const user = userEvent.setup()
|
||||
renderPage()
|
||||
|
||||
await user.click(screen.getByText('Compare the refund policies'))
|
||||
|
||||
await waitFor(() => expect(apiMock.streamCapability).toHaveBeenCalledTimes(2))
|
||||
expect(apiMock.streamResearchEvents).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({ cursor: '3' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('refreshes final partials only once when a streamed task completes', async () => {
|
||||
const activeTask = {
|
||||
cost: {},
|
||||
created_at: 1_800_000_000,
|
||||
id: 'research-active',
|
||||
knowledge_space_id: 'space-1',
|
||||
metadata: {},
|
||||
mode: 'research',
|
||||
query: 'Compare the refund policies',
|
||||
stage: 'retrieving',
|
||||
updated_at: 1_800_000_005,
|
||||
}
|
||||
apiMock.researchTasks = [activeTask]
|
||||
apiMock.refetchTasks.mockImplementation(async () => {
|
||||
apiMock.researchTasks = [
|
||||
{
|
||||
...activeTask,
|
||||
completed_at: 1_800_000_025,
|
||||
stage: 'completed',
|
||||
updated_at: 1_800_000_025,
|
||||
},
|
||||
]
|
||||
})
|
||||
apiMock.streamResearchEvents.mockImplementation(
|
||||
async ({
|
||||
onEvent,
|
||||
}: {
|
||||
onEvent: (event: {
|
||||
createdAt: string
|
||||
id: string
|
||||
payload: Record<string, unknown>
|
||||
researchTaskJobId: string
|
||||
sequence: number
|
||||
stage: string
|
||||
type: string
|
||||
}) => void
|
||||
}) => {
|
||||
onEvent({
|
||||
createdAt: '2027-01-15T08:00:25.000Z',
|
||||
id: 'event-completed',
|
||||
payload: {},
|
||||
researchTaskJobId: 'research-active',
|
||||
sequence: 5,
|
||||
stage: 'completed',
|
||||
type: 'research_task.stage_changed',
|
||||
})
|
||||
return { cursor: '5', reconnect: false, terminal: true }
|
||||
},
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
renderPage()
|
||||
|
||||
await user.click(screen.getByText('Compare the refund policies'))
|
||||
|
||||
await waitFor(() => expect(apiMock.refetchTasks).toHaveBeenCalledOnce())
|
||||
await waitFor(() => expect(apiMock.refetchPartials).toHaveBeenCalledOnce())
|
||||
})
|
||||
|
||||
it('does not let the composer shortcut bypass an active research task', async () => {
|
||||
apiMock.researchTasks = [
|
||||
{
|
||||
cost: {},
|
||||
created_at: 1_800_000_000,
|
||||
id: 'research-active',
|
||||
knowledge_space_id: 'space-1',
|
||||
metadata: {},
|
||||
mode: 'research',
|
||||
query: 'Compare the refund policies',
|
||||
stage: 'retrieving',
|
||||
updated_at: 1_800_000_005,
|
||||
},
|
||||
]
|
||||
const user = userEvent.setup()
|
||||
renderPage()
|
||||
|
||||
expect(
|
||||
screen.getByText('dataset.newKnowledge.retrievalTest.retrievingActive · 2/4'),
|
||||
).toBeInTheDocument()
|
||||
await user.click(screen.getByText('Compare the refund policies'))
|
||||
const queryInput = screen.getByLabelText('dataset.newKnowledge.retrievalTest.queryPlaceholder')
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'dataset.newKnowledge.retrievalTest.startResearch' }),
|
||||
).toBeDisabled()
|
||||
|
||||
await user.click(queryInput)
|
||||
await user.keyboard('{Control>}{Enter}{/Control}')
|
||||
|
||||
expect(apiMock.planResearch).not.toHaveBeenCalled()
|
||||
expect(apiMock.queryAdmission).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('persists a selected trace as a production bad case', async () => {
|
||||
apiMock.traces = [
|
||||
{
|
||||
@ -333,7 +555,7 @@ describe('RetrievalTestPage', () => {
|
||||
|
||||
expect(
|
||||
screen.getByRole('heading', {
|
||||
name: 'dataset.newKnowledge.retrievalTest.result:{"mode":"dataset.newKnowledge.settings.retrievalMode.deep"}',
|
||||
name: 'dataset.newKnowledge.retrievalTest.result',
|
||||
}),
|
||||
).toBeInTheDocument()
|
||||
await user.click(
|
||||
|
||||
@ -4,15 +4,18 @@ import type {
|
||||
KnowledgeFsResearchTaskPlanResponse,
|
||||
KnowledgeFsResearchTaskResponse,
|
||||
} from '@dify/contracts/api/console/knowledge-fs/types.gen'
|
||||
import type { Hotkey } from '@tanstack/react-hotkeys'
|
||||
import type {
|
||||
RetrievalEvidence,
|
||||
RetrievalTestMode,
|
||||
RetrievalTestRecord,
|
||||
} from './retrieval-test-model'
|
||||
import type { KnowledgeQueryEvent } from './services/knowledge-query-events'
|
||||
import type { ResearchTaskProgressEvent } from './services/research-task-events'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { matchesKeyboardEvent } from '@tanstack/react-hotkeys'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -30,6 +33,7 @@ import {
|
||||
} from './retrieval-test-model'
|
||||
import { newKnowledgeDocumentDetailPath, newKnowledgeQualityPath } from './routes'
|
||||
import { streamKnowledgeQuery } from './services/knowledge-query-events'
|
||||
import { streamResearchTaskEvents } from './services/research-task-events'
|
||||
|
||||
type LocalQueryRun = {
|
||||
endedAt?: number
|
||||
@ -51,11 +55,22 @@ type SelectedRun = {
|
||||
type QualityDecision = 'bad-case' | 'golden'
|
||||
|
||||
const researchStageOrder = ['planning', 'retrieving', 'analyzing', 'generating'] as const
|
||||
type ResearchStage = (typeof researchStageOrder)[number]
|
||||
const runRetrievalHotkey = 'Mod+Enter' satisfies Hotkey
|
||||
|
||||
function timeValue(value: number) {
|
||||
return value < 10_000_000_000 ? value * 1000 : value
|
||||
}
|
||||
|
||||
function formatRecordTime(value: number) {
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
month: 'short',
|
||||
}).format(value)
|
||||
}
|
||||
|
||||
function useClock(enabled: boolean) {
|
||||
const [now, setNow] = useState(() => Date.now())
|
||||
useEffect(() => {
|
||||
@ -66,6 +81,72 @@ function useClock(enabled: boolean) {
|
||||
return now
|
||||
}
|
||||
|
||||
function researchStageIndex(stage: KnowledgeFsResearchTaskResponse['stage']) {
|
||||
if (stage === 'queued' || stage === 'paused') return 0
|
||||
if (stage === 'completed') return researchStageOrder.length
|
||||
return researchStageOrder.findIndex((item) => item === stage)
|
||||
}
|
||||
|
||||
function estimatedStageDuration(
|
||||
plan: KnowledgeFsResearchTaskPlanResponse | undefined,
|
||||
stage: ResearchStage,
|
||||
) {
|
||||
if (!plan) return undefined
|
||||
const stepNames: Record<ResearchStage, Set<string>> = {
|
||||
analyzing: new Set(['analyze']),
|
||||
generating: new Set(['generate']),
|
||||
planning: new Set(['plan']),
|
||||
retrieving: new Set(['inspect', 'retrieve']),
|
||||
}
|
||||
const milliseconds = plan.steps.reduce((total, step) => {
|
||||
if (!stepNames[stage].has(typeof step.name === 'string' ? step.name : '')) return total
|
||||
return total + (typeof step.estimatedLatencyMs === 'number' ? step.estimatedLatencyMs : 0)
|
||||
}, 0)
|
||||
return milliseconds > 0 ? formatDuration(milliseconds) : undefined
|
||||
}
|
||||
|
||||
function researchProgressTime(event: ResearchTaskProgressEvent) {
|
||||
return Date.parse(event.createdAt)
|
||||
}
|
||||
|
||||
function actualStageDuration(
|
||||
events: ResearchTaskProgressEvent[],
|
||||
stage: ResearchStage,
|
||||
task: KnowledgeFsResearchTaskResponse,
|
||||
now: number,
|
||||
) {
|
||||
const start = events.find((event) => event.stage === stage)
|
||||
if (!start) return
|
||||
const startedAt = researchProgressTime(start)
|
||||
const next = events.find((event) => {
|
||||
if (event.sequence <= start.sequence) return false
|
||||
return (
|
||||
event.stage === 'canceled' ||
|
||||
event.stage === 'completed' ||
|
||||
event.stage === 'failed' ||
|
||||
(researchStageOrder.includes(event.stage as ResearchStage) && event.stage !== stage)
|
||||
)
|
||||
})
|
||||
const endedAt = next
|
||||
? researchProgressTime(next)
|
||||
: task.stage === stage
|
||||
? now
|
||||
: task.completed_at
|
||||
? timeValue(task.completed_at)
|
||||
: undefined
|
||||
if (endedAt === undefined || endedAt < startedAt) return
|
||||
return formatDuration(endedAt - startedAt)
|
||||
}
|
||||
|
||||
function mergeResearchProgressEvent(
|
||||
events: ResearchTaskProgressEvent[],
|
||||
event: ResearchTaskProgressEvent,
|
||||
) {
|
||||
const next = events.filter((candidate) => candidate.sequence !== event.sequence)
|
||||
next.push(event)
|
||||
return next.sort((left, right) => left.sequence - right.sequence)
|
||||
}
|
||||
|
||||
function ScorePill({ score }: { score: number }) {
|
||||
const normalized = Math.max(0, Math.min(1, score))
|
||||
return (
|
||||
@ -132,7 +213,7 @@ function EvidenceCard({
|
||||
</h3>
|
||||
{evidence.score !== undefined && <ScorePill score={evidence.score} />}
|
||||
</div>
|
||||
<p className="px-3 pt-1 pb-2 body-sm-regular text-text-secondary">
|
||||
<p className="px-3 pt-1 pb-2 body-md-regular tracking-[-0.07px] text-text-secondary">
|
||||
<span className="line-clamp-2 whitespace-pre-wrap">{evidence.text}</span>
|
||||
</p>
|
||||
{evidence.images.length > 0 && (
|
||||
@ -155,7 +236,7 @@ function EvidenceCard({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<footer className="flex min-h-10 items-center gap-1.5 border-t border-divider-subtle py-2 pr-2 pl-3">
|
||||
<footer className="flex h-10 items-center gap-1.5 border-t border-divider-subtle py-2 pr-2 pl-3">
|
||||
<span
|
||||
aria-hidden
|
||||
className="i-ri-file-pdf-2-fill size-4 shrink-0 text-util-colors-red-red-500"
|
||||
@ -294,7 +375,7 @@ function QualityActions({
|
||||
<Button
|
||||
disabled={pending}
|
||||
loading={pending}
|
||||
variant={noResults ? 'primary' : 'ghost'}
|
||||
variant={noResults ? 'primary' : 'secondary'}
|
||||
onClick={() => void onDecision('bad-case')}
|
||||
>
|
||||
<span aria-hidden className="mr-1 i-ri-thumb-down-line size-4" />
|
||||
@ -316,12 +397,16 @@ function QualityActions({
|
||||
}
|
||||
|
||||
function ResearchProcess({
|
||||
evidenceCount,
|
||||
events,
|
||||
expanded,
|
||||
onCancel,
|
||||
onToggle,
|
||||
plan,
|
||||
task,
|
||||
}: {
|
||||
evidenceCount: number
|
||||
events: ResearchTaskProgressEvent[]
|
||||
expanded: boolean
|
||||
onCancel?: () => void
|
||||
onToggle: () => void
|
||||
@ -331,10 +416,25 @@ function ResearchProcess({
|
||||
const { t } = useTranslation('dataset')
|
||||
const active = researchTaskIsActive(task)
|
||||
const now = useClock(active)
|
||||
const startedAt = timeValue(task.created_at)
|
||||
const endedAt = task.completed_at ? timeValue(task.completed_at) : now
|
||||
const firstProgressAt = events[0] ? researchProgressTime(events[0]) : undefined
|
||||
const terminalProgress = [...events]
|
||||
.reverse()
|
||||
.find(
|
||||
(event) =>
|
||||
event.stage === 'canceled' || event.stage === 'completed' || event.stage === 'failed',
|
||||
)
|
||||
const startedAt = firstProgressAt ?? timeValue(task.created_at)
|
||||
const endedAt = terminalProgress
|
||||
? researchProgressTime(terminalProgress)
|
||||
: task.completed_at
|
||||
? timeValue(task.completed_at)
|
||||
: now
|
||||
const duration = formatDuration(endedAt - startedAt)
|
||||
const currentIndex = researchStageOrder.findIndex((stage) => stage === task.stage)
|
||||
const currentIndex = researchStageIndex(task.stage)
|
||||
const latestVisitedIndex = events.reduce((latest, event) => {
|
||||
const index = researchStageOrder.indexOf(event.stage as ResearchStage)
|
||||
return Math.max(latest, index)
|
||||
}, -1)
|
||||
const summary =
|
||||
task.stage === 'completed'
|
||||
? t(($) => $['newKnowledge.retrievalTest.completedIn'], { duration })
|
||||
@ -349,39 +449,47 @@ function ResearchProcess({
|
||||
planning: t(($) => $['newKnowledge.retrievalTest.planning']),
|
||||
retrieving: t(($) => $['newKnowledge.retrievalTest.retrieving']),
|
||||
}
|
||||
const activeLabels: Record<(typeof researchStageOrder)[number], string> = {
|
||||
analyzing: t(($) => $['newKnowledge.retrievalTest.analyzingActive']),
|
||||
generating: t(($) => $['newKnowledge.retrievalTest.generatingActive']),
|
||||
planning: t(($) => $['newKnowledge.retrievalTest.planningActive']),
|
||||
retrieving: t(($) => $['newKnowledge.retrievalTest.retrievingActive']),
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="overflow-hidden rounded-xl border border-components-panel-border bg-components-panel-bg">
|
||||
<section
|
||||
className={cn(
|
||||
'max-w-full overflow-hidden rounded-[10px] bg-components-panel-bg',
|
||||
expanded ? 'w-full' : 'w-fit',
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={expanded}
|
||||
className="flex min-h-14 w-full items-center gap-3 px-4 text-left outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:ring-inset"
|
||||
className="flex min-h-10 max-w-full items-center gap-1.5 px-3 py-2 text-left outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:ring-inset"
|
||||
onClick={onToggle}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'size-5 text-text-accent',
|
||||
active && 'i-ri-loader-4-line animate-spin motion-reduce:animate-none',
|
||||
task.stage === 'completed' && 'i-ri-checkbox-circle-fill',
|
||||
task.stage === 'canceled' && 'i-ri-stop-circle-fill text-text-tertiary',
|
||||
task.stage === 'failed' && 'i-ri-error-warning-fill text-text-destructive',
|
||||
)}
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block system-sm-semibold text-text-primary">{summary}</span>
|
||||
{active && (
|
||||
<span className="mt-0.5 block system-xs-regular text-text-tertiary">
|
||||
{duration}
|
||||
{plan?.budget.budget_usd !== undefined && plan.budget.budget_usd !== null
|
||||
? ` · $${plan.budget.budget_usd.toFixed(2)}`
|
||||
: ''}
|
||||
</span>
|
||||
)}
|
||||
{task.stage === 'completed' ? (
|
||||
<img src="/images/new-rag/vibe-coding-star.svg" alt="" className="size-3.5 shrink-0" />
|
||||
) : (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'size-3.5 shrink-0 text-text-accent',
|
||||
active && 'i-ri-loader-4-line animate-spin motion-reduce:animate-none',
|
||||
task.stage === 'canceled' && 'i-ri-stop-circle-fill text-text-tertiary',
|
||||
task.stage === 'failed' && 'i-ri-error-warning-fill text-text-destructive',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<span className="truncate system-sm-regular whitespace-nowrap text-text-secondary">
|
||||
{summary}
|
||||
</span>
|
||||
{active && <span className="system-xs-regular text-text-tertiary">{duration}</span>}
|
||||
{active && onCancel && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onCancel()
|
||||
@ -393,52 +501,68 @@ function ResearchProcess({
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'i-ri-arrow-down-s-line size-5 text-text-tertiary transition-transform',
|
||||
'i-ri-arrow-down-s-line size-4.5 shrink-0 text-text-tertiary transition-transform',
|
||||
expanded && 'rotate-180',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
{expanded && (
|
||||
<div className="border-t border-divider-subtle px-4 py-4">
|
||||
<div className="mb-4 flex items-center gap-4 system-xs-semibold text-text-tertiary">
|
||||
<span className="text-text-primary">
|
||||
{t(($) => $['newKnowledge.retrievalTest.processLog'])}
|
||||
</span>
|
||||
<span>{t(($) => $['newKnowledge.retrievalTest.quality'])}</span>
|
||||
</div>
|
||||
<ol className="space-y-0">
|
||||
<div className="border-t border-divider-subtle px-3 pt-2.5 pb-3.5">
|
||||
<ol>
|
||||
{researchStageOrder.map((stage, index) => {
|
||||
const completed = task.stage === 'completed' || index < currentIndex
|
||||
const current = stage === task.stage
|
||||
const completed =
|
||||
task.stage === 'completed' || index < currentIndex || index < latestVisitedIndex
|
||||
const current = index === currentIndex && active
|
||||
const stageDuration =
|
||||
actualStageDuration(events, stage, task, now) ?? estimatedStageDuration(plan, stage)
|
||||
return (
|
||||
<li key={stage} className="relative flex min-h-10 items-start gap-3">
|
||||
{index < researchStageOrder.length - 1 && (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'absolute top-5 left-1.75 h-6 w-px',
|
||||
completed ? 'bg-util-colors-blue-blue-500' : 'bg-divider-regular',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<li key={stage} className="flex items-stretch gap-2.5 overflow-hidden">
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'relative mt-0.5 size-4 shrink-0 rounded-full border-2',
|
||||
completed && 'border-util-colors-blue-blue-500 bg-util-colors-blue-blue-500',
|
||||
current &&
|
||||
active &&
|
||||
'i-ri-loader-4-line animate-spin border-0 text-text-accent motion-reduce:animate-none',
|
||||
!completed && !current && 'border-divider-deep bg-components-panel-bg',
|
||||
className="flex w-3 shrink-0 flex-col items-center overflow-hidden"
|
||||
>
|
||||
<span className="flex h-5 w-3 shrink-0 items-center justify-center">
|
||||
<span
|
||||
className={cn(
|
||||
'block size-1.75 shrink-0 rounded-full',
|
||||
completed ? 'bg-gray-400' : 'bg-divider-deep',
|
||||
current &&
|
||||
'i-ri-loader-4-line size-2.5 animate-spin rounded-none text-text-accent motion-reduce:animate-none',
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
{index < researchStageOrder.length - 1 && (
|
||||
<span className="min-h-0 w-px flex-1 bg-divider-regular" />
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'system-sm-medium',
|
||||
completed || current ? 'text-text-primary' : 'text-text-quaternary',
|
||||
'flex min-w-0 flex-1 flex-col overflow-hidden',
|
||||
index < researchStageOrder.length - 1 ? 'pb-4' : 'pb-0.5',
|
||||
)}
|
||||
>
|
||||
{labels[stage]}
|
||||
<span className="flex min-h-5 items-start justify-between gap-3">
|
||||
<span
|
||||
className={cn(
|
||||
'text-[13px] leading-5 font-medium text-text-tertiary',
|
||||
(completed || current) && 'text-text-primary',
|
||||
)}
|
||||
>
|
||||
{current ? activeLabels[stage] : labels[stage]}
|
||||
</span>
|
||||
{stageDuration && (completed || current) && (
|
||||
<span className="shrink-0 system-xs-regular text-text-tertiary">
|
||||
{stageDuration}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{current && stage === 'retrieving' && evidenceCount > 0 && (
|
||||
<span className="mt-1.5 system-xs-regular text-text-tertiary">
|
||||
{t(($) => $['newKnowledge.retrievalTest.foundSoFar'], {
|
||||
count: evidenceCount,
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
)
|
||||
@ -462,6 +586,12 @@ function RecordButton({
|
||||
record: RetrievalTestRecord
|
||||
}) {
|
||||
const { t } = useTranslation('dataset')
|
||||
const activeResearchStage =
|
||||
record.kind === 'research' && record.status === 'running'
|
||||
? researchStageOrder[
|
||||
Math.min(Math.max(researchStageIndex(record.stage), 0), researchStageOrder.length - 1)
|
||||
]
|
||||
: undefined
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
@ -478,15 +608,18 @@ function RecordButton({
|
||||
<span className="line-clamp-1 system-sm-semibold text-text-secondary">{record.query}</span>
|
||||
<span className="mt-1.5 flex items-center gap-1 system-xs-regular text-text-tertiary">
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{t(($) => $[`newKnowledge.settings.retrievalMode.${record.mode}`])}
|
||||
{activeResearchStage ? (
|
||||
<>
|
||||
{t(($) => $[`newKnowledge.retrievalTest.${activeResearchStage}Active`])}
|
||||
{' · '}
|
||||
{researchStageOrder.indexOf(activeResearchStage) + 1}/{researchStageOrder.length}
|
||||
</>
|
||||
) : (
|
||||
t(($) => $[`newKnowledge.settings.retrievalMode.${record.mode}`])
|
||||
)}
|
||||
</span>
|
||||
<span className="shrink-0 text-[11px] leading-4 text-text-primary opacity-30">
|
||||
{new Intl.DateTimeFormat(undefined, {
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
month: 'short',
|
||||
}).format(record.createdAt)}
|
||||
{formatRecordTime(record.createdAt)}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
@ -508,6 +641,9 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
const [researchPlans, setResearchPlans] = useState<
|
||||
Record<string, KnowledgeFsResearchTaskPlanResponse>
|
||||
>({})
|
||||
const [researchEvents, setResearchEvents] = useState<Record<string, ResearchTaskProgressEvent[]>>(
|
||||
{},
|
||||
)
|
||||
const [researchExpanded, setResearchExpanded] = useState<Record<string, boolean>>({})
|
||||
const [qualityDecisions, setQualityDecisions] = useState<Record<string, QualityDecision>>({})
|
||||
const [qualityPendingKey, setQualityPendingKey] = useState<string>()
|
||||
@ -566,6 +702,14 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
selected?.kind === 'research'
|
||||
? researchTasksQuery.data?.data.find((task) => task.id === selected.id)
|
||||
: undefined
|
||||
const selectedResearchActive = researchTaskIsActive(selectedResearchTask)
|
||||
const selectedResearchActiveRef = useRef(selectedResearchActive)
|
||||
useEffect(() => {
|
||||
selectedResearchActiveRef.current = selectedResearchActive
|
||||
}, [selectedResearchActive])
|
||||
const selectedResearchExpanded = selectedResearchTask
|
||||
? (researchExpanded[selectedResearchTask.id] ?? true)
|
||||
: false
|
||||
const selectedTraceId =
|
||||
selected?.kind === 'trace'
|
||||
? selected.id
|
||||
@ -612,6 +756,7 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
refetchInterval: researchTaskIsActive(selectedResearchTask) ? 1000 : false,
|
||||
})
|
||||
const refetchResearchPartials = researchPartialsQuery.refetch
|
||||
const refetchResearchTasks = researchTasksQuery.refetch
|
||||
const previousSelectedResearchTaskRef = useRef<KnowledgeFsResearchTaskResponse | undefined>(
|
||||
undefined,
|
||||
)
|
||||
@ -622,6 +767,50 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
void refetchResearchPartials()
|
||||
}, [refetchResearchPartials, selectedResearchTask])
|
||||
|
||||
const selectedResearchTaskId = selectedResearchTask?.id
|
||||
useEffect(() => {
|
||||
if (!selectedResearchTaskId) return
|
||||
const controller = new AbortController()
|
||||
void (async () => {
|
||||
try {
|
||||
let cursor: string | undefined
|
||||
while (!controller.signal.aborted) {
|
||||
const capability = await consoleClient.knowledgeFs.tasks.byTaskId.streamCapability.post({
|
||||
body: { control_space_id: knowledgeSpaceId },
|
||||
params: { task_id: selectedResearchTaskId },
|
||||
})
|
||||
const stream = await streamResearchTaskEvents({
|
||||
capability,
|
||||
...(cursor ? { cursor } : {}),
|
||||
onEvent: (event) => {
|
||||
if (controller.signal.aborted) return
|
||||
setResearchEvents((current) => ({
|
||||
...current,
|
||||
[selectedResearchTaskId]: mergeResearchProgressEvent(
|
||||
current[selectedResearchTaskId] ?? [],
|
||||
event,
|
||||
),
|
||||
}))
|
||||
const terminal =
|
||||
event.stage === 'canceled' ||
|
||||
event.stage === 'completed' ||
|
||||
event.stage === 'failed'
|
||||
if (!terminal || !selectedResearchActiveRef.current) return
|
||||
if (event.stage === 'completed') void refetchResearchTasks()
|
||||
else void Promise.all([refetchResearchTasks(), refetchResearchPartials()])
|
||||
},
|
||||
signal: controller.signal,
|
||||
})
|
||||
if (stream.terminal || !stream.reconnect || !stream.cursor) return
|
||||
cursor = stream.cursor
|
||||
}
|
||||
} catch {
|
||||
// Task polling remains the fallback when capability streaming is unavailable.
|
||||
}
|
||||
})()
|
||||
return () => controller.abort()
|
||||
}, [knowledgeSpaceId, refetchResearchPartials, refetchResearchTasks, selectedResearchTaskId])
|
||||
|
||||
const historicalEvidence = extractRetrievalEvidence(traceEvidenceQuery.data)
|
||||
const researchEvidence = extractRetrievalEvidence(researchPartialsQuery.data)
|
||||
const currentEvidence =
|
||||
@ -669,6 +858,11 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
selected?.kind === 'local'
|
||||
? localRun?.mode
|
||||
: (selectedRecord?.mode ?? traceDetailQuery.data?.mode)
|
||||
const selectedCreatedAt =
|
||||
selected?.kind === 'local'
|
||||
? localRun?.startedAt
|
||||
: (selectedRecord?.createdAt ??
|
||||
(selectedResearchTask ? timeValue(selectedResearchTask.created_at) : undefined))
|
||||
const selectedIsLoading =
|
||||
(selected?.kind === 'local' && localRun?.status === 'running') ||
|
||||
(selected?.kind === 'trace' && selectedRecord?.status === 'running') ||
|
||||
@ -676,18 +870,13 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
(selected?.kind === 'trace' && traceEvidenceQuery.isPending)
|
||||
const selectedFailed = selected?.kind === 'local' && localRun?.status === 'failed'
|
||||
const selectedHasNoResults = selected?.kind === 'local' && localRun?.status === 'no-results'
|
||||
const visibleEvidence = showAll ? currentEvidence : currentEvidence.slice(0, 3)
|
||||
const visibleEvidence = showAll ? currentEvidence : currentEvidence.slice(0, 5)
|
||||
|
||||
const selectRecord = (record: RetrievalTestRecord) => {
|
||||
setSelected({ id: record.id, kind: record.kind })
|
||||
setQuery(record.query)
|
||||
setMode(record.mode)
|
||||
setShowAll(false)
|
||||
if (record.kind === 'research' && researchExpanded[record.id] === undefined)
|
||||
setResearchExpanded((current) => ({
|
||||
...current,
|
||||
[record.id]: record.status === 'running',
|
||||
}))
|
||||
}
|
||||
|
||||
const saveQualityDecision = async (decision: QualityDecision) => {
|
||||
@ -852,12 +1041,13 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
}
|
||||
|
||||
const run = () => {
|
||||
if (selectedResearchActive) return
|
||||
if (mode === 'research') void startResearch()
|
||||
else void runFastQuery()
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="flex h-full min-h-0 min-w-0 flex-1 flex-col overflow-hidden rounded-lg bg-components-panel-bg p-5">
|
||||
<main className="mx-1 flex h-full min-h-0 min-w-0 flex-1 flex-col overflow-hidden rounded-lg bg-components-panel-bg p-5">
|
||||
<header className="shrink-0">
|
||||
<h1 className="title-xl-semi-bold leading-6 text-text-primary">
|
||||
{t(($) => $['newKnowledge.retrievalTest.title'])}
|
||||
@ -882,7 +1072,7 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
className="block h-36 w-full resize-none bg-transparent p-3.5 body-md-regular text-text-primary outline-hidden placeholder:text-text-quaternary"
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
||||
if (matchesKeyboardEvent(event.nativeEvent, runRetrievalHotkey)) {
|
||||
event.preventDefault()
|
||||
run()
|
||||
}
|
||||
@ -898,10 +1088,12 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
<button
|
||||
key={item}
|
||||
type="button"
|
||||
disabled={selectedResearchActive}
|
||||
aria-pressed={mode === item}
|
||||
className={cn(
|
||||
'rounded-md px-2.5 py-1 system-xs-semibold text-text-tertiary capitalize outline-hidden hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid',
|
||||
mode === item && 'bg-components-panel-bg text-text-primary shadow-xs',
|
||||
'rounded-md px-2.5 py-1 system-sm-regular text-text-tertiary capitalize outline-hidden hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid disabled:cursor-not-allowed disabled:text-text-disabled',
|
||||
mode === item &&
|
||||
'bg-components-panel-bg font-medium text-text-primary shadow-xs',
|
||||
)}
|
||||
onClick={() => setMode(item)}
|
||||
>
|
||||
@ -909,7 +1101,11 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Button variant="primary" disabled={!query.trim()} onClick={run}>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={!query.trim() || selectedResearchActive}
|
||||
onClick={run}
|
||||
>
|
||||
<span aria-hidden className="mr-1 i-ri-play-circle-line size-4" />
|
||||
{t(($) =>
|
||||
mode === 'research'
|
||||
@ -927,7 +1123,7 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
{t(($) => $['newKnowledge.retrievalTest.records'])}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||
<div className="min-h-0 flex-1 scrollbar-none overflow-y-auto">
|
||||
{displayRecords.length > 0 ? (
|
||||
<div>
|
||||
{displayRecords.map((record, index) => (
|
||||
@ -958,46 +1154,64 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
)}
|
||||
{selected && (
|
||||
<div className="flex h-full min-h-0 flex-col gap-3">
|
||||
<div className="flex h-5 shrink-0 items-center gap-2 overflow-hidden pl-3">
|
||||
<div className="flex h-6 shrink-0 items-center gap-2 overflow-hidden pl-3">
|
||||
<h2 className="shrink-0 system-sm-semibold text-text-primary">
|
||||
{selected?.kind === 'research'
|
||||
? t(($) => $['newKnowledge.retrievalTest.researchResult'])
|
||||
: t(($) => $['newKnowledge.retrievalTest.result'], {
|
||||
mode: selectedMode
|
||||
? t(($) => $[`newKnowledge.settings.retrievalMode.${selectedMode}`])
|
||||
: '',
|
||||
})}
|
||||
: t(($) => $['newKnowledge.retrievalTest.result'])}
|
||||
</h2>
|
||||
<span className="shrink-0 rounded-md bg-divider-regular px-1.5 py-0.5 text-[11px] leading-4 font-medium text-text-tertiary capitalize">
|
||||
{selectedMode
|
||||
? t(($) => $[`newKnowledge.settings.retrievalMode.${selectedMode}`])
|
||||
: ''}
|
||||
</span>
|
||||
<span className="shrink-0 text-[11px] leading-4 text-text-tertiary">
|
||||
{t(($) => $['newKnowledge.retrievalTest.justNow'])}
|
||||
</span>
|
||||
{selectedCreatedAt && (
|
||||
<span className="shrink-0 text-[11px] leading-4 text-text-tertiary">
|
||||
{formatRecordTime(selectedCreatedAt)}
|
||||
</span>
|
||||
)}
|
||||
<span className="min-w-0 flex-1" />
|
||||
{selectedResearchTask && (
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={selectedResearchExpanded}
|
||||
className="flex h-6 shrink-0 items-center gap-1 rounded-md px-1.5 system-xs-medium text-text-tertiary outline-hidden hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid"
|
||||
onClick={() =>
|
||||
setResearchExpanded((current) => ({
|
||||
...current,
|
||||
[selectedResearchTask.id]: !(current[selectedResearchTask.id] ?? true),
|
||||
}))
|
||||
}
|
||||
>
|
||||
<span aria-hidden className="i-ri-search-eye-line size-3.5" />
|
||||
{t(($) => $['newKnowledge.retrievalTest.processLog'])}
|
||||
</button>
|
||||
)}
|
||||
<Link
|
||||
href={newKnowledgeQualityPath(knowledgeSpaceId)}
|
||||
className="flex h-6 shrink-0 items-center gap-1 rounded-md px-1.5 system-xs-medium text-text-tertiary outline-hidden hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid"
|
||||
>
|
||||
<span aria-hidden className="i-ri-equalizer-2-line size-3.5" />
|
||||
{t(($) => $['newKnowledge.retrievalTest.quality'])}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||
<div className="min-h-0 flex-1 scrollbar-none overflow-y-auto">
|
||||
{selectedResearchTask && (
|
||||
<ResearchProcess
|
||||
task={selectedResearchTask}
|
||||
plan={researchPlans[selectedResearchTask.id]}
|
||||
expanded={
|
||||
researchExpanded[selectedResearchTask.id] ??
|
||||
researchTaskIsActive(selectedResearchTask)
|
||||
}
|
||||
events={researchEvents[selectedResearchTask.id] ?? []}
|
||||
evidenceCount={currentEvidence.length}
|
||||
expanded={selectedResearchExpanded}
|
||||
onToggle={() =>
|
||||
setResearchExpanded((current) => ({
|
||||
...current,
|
||||
[selectedResearchTask.id]: !(
|
||||
current[selectedResearchTask.id] ??
|
||||
researchTaskIsActive(selectedResearchTask)
|
||||
),
|
||||
[selectedResearchTask.id]: !(current[selectedResearchTask.id] ?? true),
|
||||
}))
|
||||
}
|
||||
onCancel={
|
||||
researchTaskIsActive(selectedResearchTask)
|
||||
selectedResearchActive
|
||||
? () => void cancelResearch(selectedResearchTask.id)
|
||||
: undefined
|
||||
}
|
||||
@ -1027,14 +1241,12 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
)}
|
||||
|
||||
{currentEvidence.length > 0 && (
|
||||
<div className={cn(selectedResearchTask && 'mt-5')}>
|
||||
{selectedResearchTask && (
|
||||
<h3 className="mb-3 system-sm-semibold text-text-secondary">
|
||||
{researchTaskIsActive(selectedResearchTask)
|
||||
? t(($) => $['newKnowledge.retrievalTest.foundSoFar'], {
|
||||
count: currentEvidence.length,
|
||||
})
|
||||
: `${currentEvidence.length} chunks`}
|
||||
<div className={cn(selectedResearchTask && 'mt-3')}>
|
||||
{selectedResearchActive && (
|
||||
<h3 className="flex h-6 items-start pb-2 pl-3 system-xs-medium text-text-tertiary">
|
||||
{t(($) => $['newKnowledge.retrievalTest.foundSoFar'], {
|
||||
count: currentEvidence.length,
|
||||
})}
|
||||
</h3>
|
||||
)}
|
||||
<div className="space-y-3">
|
||||
@ -1052,9 +1264,12 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
/>
|
||||
))}
|
||||
{selectedResearchTask && researchTaskIsActive(selectedResearchTask) && (
|
||||
<div className="h-24 animate-pulse rounded-xl border border-components-panel-border bg-components-panel-bg p-4 motion-reduce:animate-none">
|
||||
<div className="h-3 w-1/3 rounded bg-background-section-burn" />
|
||||
<div className="mt-4 h-3 w-4/5 rounded bg-background-section-burn" />
|
||||
<div className="h-16.5 animate-pulse rounded-xl bg-components-panel-bg px-3 py-3.5 opacity-60 motion-reduce:animate-none">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="h-3 w-30 rounded-xs bg-divider-regular" />
|
||||
<div className="h-4 w-14 rounded-md bg-divider-subtle" />
|
||||
</div>
|
||||
<div className="mt-2.5 h-3 w-full rounded-xs bg-divider-subtle" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -1062,7 +1277,7 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!showAll && currentEvidence.length > 3 && (
|
||||
{!showAll && currentEvidence.length > 5 && (
|
||||
<div className="shrink-0 pl-1">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@ -34,7 +34,7 @@ function parseEventBlock(block: string): KnowledgeQueryEvent | undefined {
|
||||
|
||||
function capabilityTraceId(token: string) {
|
||||
const payload = token.split('.')[1]
|
||||
if (!payload) throw new Error('Knowledge query capability is malformed')
|
||||
if (!payload) throw new Error('KnowledgeFS stream capability is malformed')
|
||||
try {
|
||||
const normalized = payload.replace(/-/g, '+').replace(/_/g, '/')
|
||||
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=')
|
||||
@ -51,32 +51,39 @@ function capabilityTraceId(token: string) {
|
||||
} catch {
|
||||
// The signed token is verified by KnowledgeFS; the client only echoes its public trace claim.
|
||||
}
|
||||
throw new Error('Knowledge query capability is missing its trace binding')
|
||||
throw new Error('KnowledgeFS stream capability is missing its trace binding')
|
||||
}
|
||||
|
||||
export async function streamKnowledgeQuery({
|
||||
admission,
|
||||
export async function streamCapabilityEvents({
|
||||
body,
|
||||
method = 'GET',
|
||||
onEvent,
|
||||
signal,
|
||||
token,
|
||||
url,
|
||||
}: {
|
||||
admission: KnowledgeFsQueryAdmissionResponse
|
||||
body?: unknown
|
||||
method?: 'GET' | 'POST'
|
||||
onEvent: (event: KnowledgeQueryEvent) => void
|
||||
signal?: AbortSignal
|
||||
token: string
|
||||
url: string
|
||||
}) {
|
||||
const response = await fetch(admission.url, {
|
||||
body: JSON.stringify(admission.request),
|
||||
const headers: Record<string, string> = {
|
||||
Accept: 'text/event-stream',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'X-Trace-ID': capabilityTraceId(token),
|
||||
}
|
||||
if (body !== undefined) headers['Content-Type'] = 'application/json'
|
||||
const response = await fetch(url, {
|
||||
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
||||
credentials: 'omit',
|
||||
headers: {
|
||||
Accept: 'text/event-stream',
|
||||
Authorization: `Bearer ${admission.token}`,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Trace-ID': capabilityTraceId(admission.token),
|
||||
},
|
||||
method: 'POST',
|
||||
headers,
|
||||
method,
|
||||
signal,
|
||||
})
|
||||
if (!response.ok) throw response
|
||||
if (!response.body) throw new Error('Knowledge query stream has no response body')
|
||||
if (!response.body) throw new Error('KnowledgeFS event stream has no response body')
|
||||
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
@ -97,3 +104,22 @@ export async function streamKnowledgeQuery({
|
||||
const trailingEvent = parseEventBlock(buffer)
|
||||
if (trailingEvent) onEvent(trailingEvent)
|
||||
}
|
||||
|
||||
export async function streamKnowledgeQuery({
|
||||
admission,
|
||||
onEvent,
|
||||
signal,
|
||||
}: {
|
||||
admission: KnowledgeFsQueryAdmissionResponse
|
||||
onEvent: (event: KnowledgeQueryEvent) => void
|
||||
signal?: AbortSignal
|
||||
}) {
|
||||
await streamCapabilityEvents({
|
||||
body: admission.request,
|
||||
method: 'POST',
|
||||
onEvent,
|
||||
signal,
|
||||
token: admission.token,
|
||||
url: admission.url,
|
||||
})
|
||||
}
|
||||
|
||||
131
web/features/new-rag/services/research-task-events.ts
Normal file
131
web/features/new-rag/services/research-task-events.ts
Normal file
@ -0,0 +1,131 @@
|
||||
import type {
|
||||
KnowledgeFsResearchTaskResponse,
|
||||
KnowledgeFsStreamCapabilityResponse,
|
||||
} from '@dify/contracts/api/console/knowledge-fs/types.gen'
|
||||
import { streamCapabilityEvents } from './knowledge-query-events'
|
||||
|
||||
export type ResearchTaskProgressEvent = {
|
||||
createdAt: string
|
||||
id: string
|
||||
payload: Record<string, unknown>
|
||||
researchTaskJobId: string
|
||||
sequence: number
|
||||
stage: KnowledgeFsResearchTaskResponse['stage']
|
||||
type:
|
||||
| 'research_task.canceled'
|
||||
| 'research_task.failed'
|
||||
| 'research_task.paused'
|
||||
| 'research_task.resumed'
|
||||
| 'research_task.stage_changed'
|
||||
| 'research_task.started'
|
||||
}
|
||||
|
||||
export type ResearchTaskEventStreamResult = {
|
||||
cursor?: string
|
||||
reconnect: boolean
|
||||
terminal: boolean
|
||||
}
|
||||
|
||||
const researchStages = new Set<KnowledgeFsResearchTaskResponse['stage']>([
|
||||
'analyzing',
|
||||
'canceled',
|
||||
'completed',
|
||||
'failed',
|
||||
'generating',
|
||||
'paused',
|
||||
'planning',
|
||||
'queued',
|
||||
'retrieving',
|
||||
])
|
||||
|
||||
const progressEventTypes = new Set<ResearchTaskProgressEvent['type']>([
|
||||
'research_task.canceled',
|
||||
'research_task.failed',
|
||||
'research_task.paused',
|
||||
'research_task.resumed',
|
||||
'research_task.stage_changed',
|
||||
'research_task.started',
|
||||
])
|
||||
|
||||
function progressEvent(value: unknown): ResearchTaskProgressEvent | undefined {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return
|
||||
const event = value as Record<string, unknown>
|
||||
if (
|
||||
typeof event.createdAt !== 'string' ||
|
||||
!Number.isFinite(Date.parse(event.createdAt)) ||
|
||||
typeof event.id !== 'string' ||
|
||||
!event.id ||
|
||||
typeof event.researchTaskJobId !== 'string' ||
|
||||
!event.researchTaskJobId ||
|
||||
typeof event.sequence !== 'number' ||
|
||||
!Number.isInteger(event.sequence) ||
|
||||
typeof event.stage !== 'string' ||
|
||||
!researchStages.has(event.stage as KnowledgeFsResearchTaskResponse['stage']) ||
|
||||
typeof event.type !== 'string' ||
|
||||
!progressEventTypes.has(event.type as ResearchTaskProgressEvent['type'])
|
||||
)
|
||||
return
|
||||
const payload =
|
||||
event.payload && typeof event.payload === 'object' && !Array.isArray(event.payload)
|
||||
? (event.payload as Record<string, unknown>)
|
||||
: {}
|
||||
return {
|
||||
createdAt: event.createdAt,
|
||||
id: event.id,
|
||||
payload,
|
||||
researchTaskJobId: event.researchTaskJobId,
|
||||
sequence: event.sequence,
|
||||
stage: event.stage as KnowledgeFsResearchTaskResponse['stage'],
|
||||
type: event.type as ResearchTaskProgressEvent['type'],
|
||||
}
|
||||
}
|
||||
|
||||
function streamUrl(url: string, cursor: string | undefined) {
|
||||
if (!cursor) return url
|
||||
return `${url}${url.includes('?') ? '&' : '?'}cursor=${encodeURIComponent(cursor)}`
|
||||
}
|
||||
|
||||
function timeoutCursor(data: unknown) {
|
||||
if (!data || typeof data !== 'object') return
|
||||
const cursor = (data as Record<string, unknown>).cursor
|
||||
return typeof cursor === 'string' && cursor ? cursor : undefined
|
||||
}
|
||||
|
||||
export async function streamResearchTaskEvents({
|
||||
capability,
|
||||
cursor,
|
||||
onEvent,
|
||||
signal,
|
||||
}: {
|
||||
capability: KnowledgeFsStreamCapabilityResponse
|
||||
cursor?: string
|
||||
onEvent: (event: ResearchTaskProgressEvent) => void
|
||||
signal?: AbortSignal
|
||||
}): Promise<ResearchTaskEventStreamResult> {
|
||||
let nextCursor = cursor
|
||||
let reconnectAfterTimeout = false
|
||||
let terminal = false
|
||||
await streamCapabilityEvents({
|
||||
onEvent: (event) => {
|
||||
if (event.event === 'timeout') {
|
||||
nextCursor = timeoutCursor(event.data) ?? nextCursor
|
||||
reconnectAfterTimeout = true
|
||||
return
|
||||
}
|
||||
const parsed = progressEvent(event.data)
|
||||
if (!parsed) return
|
||||
nextCursor = String(parsed.sequence)
|
||||
terminal =
|
||||
parsed.stage === 'canceled' || parsed.stage === 'completed' || parsed.stage === 'failed'
|
||||
onEvent(parsed)
|
||||
},
|
||||
signal,
|
||||
token: capability.token,
|
||||
url: streamUrl(capability.url, cursor),
|
||||
})
|
||||
return {
|
||||
...(nextCursor ? { cursor: nextCursor } : {}),
|
||||
reconnect: !terminal && (reconnectAfterTimeout || nextCursor !== cursor),
|
||||
terminal,
|
||||
}
|
||||
}
|
||||
@ -504,6 +504,7 @@
|
||||
"newKnowledge.retrievalCount": "عدد مرات الاسترجاع",
|
||||
"newKnowledge.retrievalCountValue": "{{value}} خلال آخر 7 أيام",
|
||||
"newKnowledge.retrievalTest.analyzing": "تحليل الأدلة",
|
||||
"newKnowledge.retrievalTest.analyzingActive": "تحليل الأدلة…",
|
||||
"newKnowledge.retrievalTest.cancel": "إلغاء",
|
||||
"newKnowledge.retrievalTest.canceled": "تم الإلغاء",
|
||||
"newKnowledge.retrievalTest.completedIn": "اكتمل خلال {{duration}}",
|
||||
@ -515,6 +516,7 @@
|
||||
"newKnowledge.retrievalTest.failedTitle": "فشل الاسترجاع",
|
||||
"newKnowledge.retrievalTest.foundSoFar": "تم العثور حتى الآن ({{count}})",
|
||||
"newKnowledge.retrievalTest.generating": "إنشاء الإجابة",
|
||||
"newKnowledge.retrievalTest.generatingActive": "إنشاء الإجابة…",
|
||||
"newKnowledge.retrievalTest.justNow": "الآن",
|
||||
"newKnowledge.retrievalTest.keepGoldenQuestion": "الاحتفاظ كسؤال مرجعي",
|
||||
"newKnowledge.retrievalTest.makeBadCase": "حفظ كحالة تحتاج إلى تحسين",
|
||||
@ -523,13 +525,15 @@
|
||||
"newKnowledge.retrievalTest.open": "فتح",
|
||||
"newKnowledge.retrievalTest.page": "الصفحة {{page}}",
|
||||
"newKnowledge.retrievalTest.planning": "تخطيط البحث",
|
||||
"newKnowledge.retrievalTest.planningActive": "تخطيط البحث…",
|
||||
"newKnowledge.retrievalTest.processLog": "سجل العملية",
|
||||
"newKnowledge.retrievalTest.quality": "الجودة",
|
||||
"newKnowledge.retrievalTest.queryPlaceholder": "أدخل استعلاماً لاختبار الاسترجاع…",
|
||||
"newKnowledge.retrievalTest.records": "السجلات",
|
||||
"newKnowledge.retrievalTest.researchResult": "نتيجة البحث",
|
||||
"newKnowledge.retrievalTest.result": "نتيجة {{mode}}",
|
||||
"newKnowledge.retrievalTest.result": "نتيجة الاسترجاع",
|
||||
"newKnowledge.retrievalTest.retrieving": "استرجاع الأدلة",
|
||||
"newKnowledge.retrievalTest.retrievingActive": "استرجاع الأدلة…",
|
||||
"newKnowledge.retrievalTest.retry": "إعادة المحاولة",
|
||||
"newKnowledge.retrievalTest.run": "تشغيل",
|
||||
"newKnowledge.retrievalTest.running": "جارٍ البحث…",
|
||||
|
||||
@ -504,6 +504,7 @@
|
||||
"newKnowledge.retrievalCount": "Anzahl der Abrufe",
|
||||
"newKnowledge.retrievalCountValue": "{{value}} in den letzten 7 Tagen",
|
||||
"newKnowledge.retrievalTest.analyzing": "Belege analysieren",
|
||||
"newKnowledge.retrievalTest.analyzingActive": "Belege analysieren…",
|
||||
"newKnowledge.retrievalTest.cancel": "Abbrechen",
|
||||
"newKnowledge.retrievalTest.canceled": "Abgebrochen",
|
||||
"newKnowledge.retrievalTest.completedIn": "Abgeschlossen in {{duration}}",
|
||||
@ -515,6 +516,7 @@
|
||||
"newKnowledge.retrievalTest.failedTitle": "Abruf fehlgeschlagen",
|
||||
"newKnowledge.retrievalTest.foundSoFar": "Bisher gefunden ({{count}})",
|
||||
"newKnowledge.retrievalTest.generating": "Antwort erstellen",
|
||||
"newKnowledge.retrievalTest.generatingActive": "Antwort erstellen…",
|
||||
"newKnowledge.retrievalTest.justNow": "Gerade eben",
|
||||
"newKnowledge.retrievalTest.keepGoldenQuestion": "Als Referenzfrage speichern",
|
||||
"newKnowledge.retrievalTest.makeBadCase": "Als Problemfall speichern",
|
||||
@ -523,13 +525,15 @@
|
||||
"newKnowledge.retrievalTest.open": "Öffnen",
|
||||
"newKnowledge.retrievalTest.page": "Seite {{page}}",
|
||||
"newKnowledge.retrievalTest.planning": "Recherche planen",
|
||||
"newKnowledge.retrievalTest.planningActive": "Recherche planen…",
|
||||
"newKnowledge.retrievalTest.processLog": "Prozessprotokoll",
|
||||
"newKnowledge.retrievalTest.quality": "Qualität",
|
||||
"newKnowledge.retrievalTest.queryPlaceholder": "Abfrage eingeben, um den Abruf zu testen…",
|
||||
"newKnowledge.retrievalTest.records": "Verlauf",
|
||||
"newKnowledge.retrievalTest.researchResult": "Rechercheergebnis",
|
||||
"newKnowledge.retrievalTest.result": "{{mode}}-Ergebnis",
|
||||
"newKnowledge.retrievalTest.result": "Suchergebnis",
|
||||
"newKnowledge.retrievalTest.retrieving": "Belege abrufen",
|
||||
"newKnowledge.retrievalTest.retrievingActive": "Belege abrufen…",
|
||||
"newKnowledge.retrievalTest.retry": "Erneut versuchen",
|
||||
"newKnowledge.retrievalTest.run": "Ausführen",
|
||||
"newKnowledge.retrievalTest.running": "Recherche läuft…",
|
||||
|
||||
@ -503,7 +503,8 @@
|
||||
"newKnowledge.removeSource": "Remove source",
|
||||
"newKnowledge.retrievalCount": "Retrieval count",
|
||||
"newKnowledge.retrievalCountValue": "{{value}} in last 7 days",
|
||||
"newKnowledge.retrievalTest.analyzing": "Analyze evidence",
|
||||
"newKnowledge.retrievalTest.analyzing": "Analyzed findings",
|
||||
"newKnowledge.retrievalTest.analyzingActive": "Analyzing findings…",
|
||||
"newKnowledge.retrievalTest.cancel": "Cancel",
|
||||
"newKnowledge.retrievalTest.canceled": "Canceled",
|
||||
"newKnowledge.retrievalTest.completedIn": "Completed in {{duration}}",
|
||||
@ -514,7 +515,8 @@
|
||||
"newKnowledge.retrievalTest.failedDescription": "The retrieval run couldn't be completed. Try the question again.",
|
||||
"newKnowledge.retrievalTest.failedTitle": "Retrieval failed",
|
||||
"newKnowledge.retrievalTest.foundSoFar": "Found so far ({{count}})",
|
||||
"newKnowledge.retrievalTest.generating": "Generate response",
|
||||
"newKnowledge.retrievalTest.generating": "Generated result",
|
||||
"newKnowledge.retrievalTest.generatingActive": "Generating result…",
|
||||
"newKnowledge.retrievalTest.justNow": "Just now",
|
||||
"newKnowledge.retrievalTest.keepGoldenQuestion": "Keep golden question",
|
||||
"newKnowledge.retrievalTest.makeBadCase": "Make bad case",
|
||||
@ -522,20 +524,22 @@
|
||||
"newKnowledge.retrievalTest.noChunksTitle": "No chunks retrieved",
|
||||
"newKnowledge.retrievalTest.open": "Open",
|
||||
"newKnowledge.retrievalTest.page": "Page {{page}}",
|
||||
"newKnowledge.retrievalTest.planning": "Plan research",
|
||||
"newKnowledge.retrievalTest.planning": "Planned the research",
|
||||
"newKnowledge.retrievalTest.planningActive": "Planning research…",
|
||||
"newKnowledge.retrievalTest.processLog": "Process log",
|
||||
"newKnowledge.retrievalTest.quality": "Quality",
|
||||
"newKnowledge.retrievalTest.queryPlaceholder": "Enter a query to test retrieval…",
|
||||
"newKnowledge.retrievalTest.records": "Records",
|
||||
"newKnowledge.retrievalTest.researchResult": "Research result",
|
||||
"newKnowledge.retrievalTest.result": "Retrieval result",
|
||||
"newKnowledge.retrievalTest.retrieving": "Retrieve evidence",
|
||||
"newKnowledge.retrievalTest.retrieving": "Retrieved sources",
|
||||
"newKnowledge.retrievalTest.retrievingActive": "Retrieving sources…",
|
||||
"newKnowledge.retrievalTest.retry": "Retry",
|
||||
"newKnowledge.retrievalTest.run": "Run",
|
||||
"newKnowledge.retrievalTest.running": "Researching…",
|
||||
"newKnowledge.retrievalTest.savedBadCase": "Saved as bad case",
|
||||
"newKnowledge.retrievalTest.savedGoldenQuestion": "Saved as golden question",
|
||||
"newKnowledge.retrievalTest.showAllChunks": "Show all {{count}} chunks",
|
||||
"newKnowledge.retrievalTest.showAllChunks": "Show all {{count}} sources",
|
||||
"newKnowledge.retrievalTest.startResearch": "Start research",
|
||||
"newKnowledge.retrievalTest.title": "Retrieval Test",
|
||||
"newKnowledge.retrievalTest.viewInQuality": "View in Quality",
|
||||
|
||||
@ -504,6 +504,7 @@
|
||||
"newKnowledge.retrievalCount": "Número de recuperaciones",
|
||||
"newKnowledge.retrievalCountValue": "{{value}} en los últimos 7 días",
|
||||
"newKnowledge.retrievalTest.analyzing": "Analizar evidencias",
|
||||
"newKnowledge.retrievalTest.analyzingActive": "Analizar evidencias…",
|
||||
"newKnowledge.retrievalTest.cancel": "Cancelar",
|
||||
"newKnowledge.retrievalTest.canceled": "Cancelado",
|
||||
"newKnowledge.retrievalTest.completedIn": "Completado en {{duration}}",
|
||||
@ -515,6 +516,7 @@
|
||||
"newKnowledge.retrievalTest.failedTitle": "Error de recuperación",
|
||||
"newKnowledge.retrievalTest.foundSoFar": "Encontrados hasta ahora ({{count}})",
|
||||
"newKnowledge.retrievalTest.generating": "Generar respuesta",
|
||||
"newKnowledge.retrievalTest.generatingActive": "Generar respuesta…",
|
||||
"newKnowledge.retrievalTest.justNow": "Ahora mismo",
|
||||
"newKnowledge.retrievalTest.keepGoldenQuestion": "Guardar como pregunta de referencia",
|
||||
"newKnowledge.retrievalTest.makeBadCase": "Guardar como caso problemático",
|
||||
@ -523,13 +525,15 @@
|
||||
"newKnowledge.retrievalTest.open": "Abrir",
|
||||
"newKnowledge.retrievalTest.page": "Página {{page}}",
|
||||
"newKnowledge.retrievalTest.planning": "Planificar investigación",
|
||||
"newKnowledge.retrievalTest.planningActive": "Planificar investigación…",
|
||||
"newKnowledge.retrievalTest.processLog": "Registro del proceso",
|
||||
"newKnowledge.retrievalTest.quality": "Calidad",
|
||||
"newKnowledge.retrievalTest.queryPlaceholder": "Introduce una consulta para probar la recuperación…",
|
||||
"newKnowledge.retrievalTest.records": "Registros",
|
||||
"newKnowledge.retrievalTest.researchResult": "Resultado de la investigación",
|
||||
"newKnowledge.retrievalTest.result": "Resultado de {{mode}}",
|
||||
"newKnowledge.retrievalTest.result": "Resultado de recuperación",
|
||||
"newKnowledge.retrievalTest.retrieving": "Recuperar evidencias",
|
||||
"newKnowledge.retrievalTest.retrievingActive": "Recuperar evidencias…",
|
||||
"newKnowledge.retrievalTest.retry": "Reintentar",
|
||||
"newKnowledge.retrievalTest.run": "Ejecutar",
|
||||
"newKnowledge.retrievalTest.running": "Investigando…",
|
||||
|
||||
@ -504,6 +504,7 @@
|
||||
"newKnowledge.retrievalCount": "تعداد بازیابی",
|
||||
"newKnowledge.retrievalCountValue": "{{value}} در ۷ روز گذشته",
|
||||
"newKnowledge.retrievalTest.analyzing": "تحلیل شواهد",
|
||||
"newKnowledge.retrievalTest.analyzingActive": "تحلیل شواهد…",
|
||||
"newKnowledge.retrievalTest.cancel": "لغو",
|
||||
"newKnowledge.retrievalTest.canceled": "لغو شد",
|
||||
"newKnowledge.retrievalTest.completedIn": "در {{duration}} تکمیل شد",
|
||||
@ -515,6 +516,7 @@
|
||||
"newKnowledge.retrievalTest.failedTitle": "بازیابی ناموفق بود",
|
||||
"newKnowledge.retrievalTest.foundSoFar": "یافتهشده تا این لحظه ({{count}})",
|
||||
"newKnowledge.retrievalTest.generating": "تولید پاسخ",
|
||||
"newKnowledge.retrievalTest.generatingActive": "تولید پاسخ…",
|
||||
"newKnowledge.retrievalTest.justNow": "همین حالا",
|
||||
"newKnowledge.retrievalTest.keepGoldenQuestion": "ذخیره بهعنوان پرسش مرجع",
|
||||
"newKnowledge.retrievalTest.makeBadCase": "ذخیره بهعنوان مورد نیازمند بهبود",
|
||||
@ -523,13 +525,15 @@
|
||||
"newKnowledge.retrievalTest.open": "باز کردن",
|
||||
"newKnowledge.retrievalTest.page": "صفحه {{page}}",
|
||||
"newKnowledge.retrievalTest.planning": "برنامهریزی پژوهش",
|
||||
"newKnowledge.retrievalTest.planningActive": "برنامهریزی پژوهش…",
|
||||
"newKnowledge.retrievalTest.processLog": "گزارش فرایند",
|
||||
"newKnowledge.retrievalTest.quality": "کیفیت",
|
||||
"newKnowledge.retrievalTest.queryPlaceholder": "برای آزمایش بازیابی، یک پرسوجو وارد کنید…",
|
||||
"newKnowledge.retrievalTest.records": "سوابق",
|
||||
"newKnowledge.retrievalTest.researchResult": "نتیجه پژوهش",
|
||||
"newKnowledge.retrievalTest.result": "نتیجه {{mode}}",
|
||||
"newKnowledge.retrievalTest.result": "نتیجه بازیابی",
|
||||
"newKnowledge.retrievalTest.retrieving": "بازیابی شواهد",
|
||||
"newKnowledge.retrievalTest.retrievingActive": "بازیابی شواهد…",
|
||||
"newKnowledge.retrievalTest.retry": "تلاش دوباره",
|
||||
"newKnowledge.retrievalTest.run": "اجرا",
|
||||
"newKnowledge.retrievalTest.running": "در حال پژوهش…",
|
||||
|
||||
@ -504,6 +504,7 @@
|
||||
"newKnowledge.retrievalCount": "Nombre de récupérations",
|
||||
"newKnowledge.retrievalCountValue": "{{value}} au cours des 7 derniers jours",
|
||||
"newKnowledge.retrievalTest.analyzing": "Analyser les éléments",
|
||||
"newKnowledge.retrievalTest.analyzingActive": "Analyser les éléments…",
|
||||
"newKnowledge.retrievalTest.cancel": "Annuler",
|
||||
"newKnowledge.retrievalTest.canceled": "Annulé",
|
||||
"newKnowledge.retrievalTest.completedIn": "Terminé en {{duration}}",
|
||||
@ -515,6 +516,7 @@
|
||||
"newKnowledge.retrievalTest.failedTitle": "Échec de la récupération",
|
||||
"newKnowledge.retrievalTest.foundSoFar": "Trouvés jusqu’ici ({{count}})",
|
||||
"newKnowledge.retrievalTest.generating": "Générer la réponse",
|
||||
"newKnowledge.retrievalTest.generatingActive": "Générer la réponse…",
|
||||
"newKnowledge.retrievalTest.justNow": "À l’instant",
|
||||
"newKnowledge.retrievalTest.keepGoldenQuestion": "Conserver comme question de référence",
|
||||
"newKnowledge.retrievalTest.makeBadCase": "Enregistrer comme cas problématique",
|
||||
@ -523,13 +525,15 @@
|
||||
"newKnowledge.retrievalTest.open": "Ouvrir",
|
||||
"newKnowledge.retrievalTest.page": "Page {{page}}",
|
||||
"newKnowledge.retrievalTest.planning": "Planifier la recherche",
|
||||
"newKnowledge.retrievalTest.planningActive": "Planifier la recherche…",
|
||||
"newKnowledge.retrievalTest.processLog": "Journal du processus",
|
||||
"newKnowledge.retrievalTest.quality": "Qualité",
|
||||
"newKnowledge.retrievalTest.queryPlaceholder": "Saisissez une requête pour tester la récupération…",
|
||||
"newKnowledge.retrievalTest.records": "Historique",
|
||||
"newKnowledge.retrievalTest.researchResult": "Résultat de la recherche",
|
||||
"newKnowledge.retrievalTest.result": "Résultat {{mode}}",
|
||||
"newKnowledge.retrievalTest.result": "Résultat de la récupération",
|
||||
"newKnowledge.retrievalTest.retrieving": "Récupérer les éléments",
|
||||
"newKnowledge.retrievalTest.retrievingActive": "Récupérer les éléments…",
|
||||
"newKnowledge.retrievalTest.retry": "Réessayer",
|
||||
"newKnowledge.retrievalTest.run": "Exécuter",
|
||||
"newKnowledge.retrievalTest.running": "Recherche en cours…",
|
||||
|
||||
@ -504,6 +504,7 @@
|
||||
"newKnowledge.retrievalCount": "पुनर्प्राप्ति संख्या",
|
||||
"newKnowledge.retrievalCountValue": "पिछले 7 दिनों में {{value}}",
|
||||
"newKnowledge.retrievalTest.analyzing": "साक्ष्य का विश्लेषण करें",
|
||||
"newKnowledge.retrievalTest.analyzingActive": "साक्ष्य का विश्लेषण करें…",
|
||||
"newKnowledge.retrievalTest.cancel": "रद्द करें",
|
||||
"newKnowledge.retrievalTest.canceled": "रद्द किया गया",
|
||||
"newKnowledge.retrievalTest.completedIn": "{{duration}} में पूरा हुआ",
|
||||
@ -515,6 +516,7 @@
|
||||
"newKnowledge.retrievalTest.failedTitle": "रिट्रीवल विफल रहा",
|
||||
"newKnowledge.retrievalTest.foundSoFar": "अब तक मिले ({{count}})",
|
||||
"newKnowledge.retrievalTest.generating": "उत्तर तैयार करें",
|
||||
"newKnowledge.retrievalTest.generatingActive": "उत्तर तैयार करें…",
|
||||
"newKnowledge.retrievalTest.justNow": "अभी",
|
||||
"newKnowledge.retrievalTest.keepGoldenQuestion": "संदर्भ प्रश्न के रूप में रखें",
|
||||
"newKnowledge.retrievalTest.makeBadCase": "सुधार योग्य मामले के रूप में सहेजें",
|
||||
@ -523,13 +525,15 @@
|
||||
"newKnowledge.retrievalTest.open": "खोलें",
|
||||
"newKnowledge.retrievalTest.page": "पृष्ठ {{page}}",
|
||||
"newKnowledge.retrievalTest.planning": "रिसर्च की योजना बनाएँ",
|
||||
"newKnowledge.retrievalTest.planningActive": "रिसर्च की योजना बनाएँ…",
|
||||
"newKnowledge.retrievalTest.processLog": "प्रक्रिया लॉग",
|
||||
"newKnowledge.retrievalTest.quality": "गुणवत्ता",
|
||||
"newKnowledge.retrievalTest.queryPlaceholder": "रिट्रीवल जाँचने के लिए क्वेरी दर्ज करें…",
|
||||
"newKnowledge.retrievalTest.records": "रिकॉर्ड",
|
||||
"newKnowledge.retrievalTest.researchResult": "रिसर्च परिणाम",
|
||||
"newKnowledge.retrievalTest.result": "{{mode}} परिणाम",
|
||||
"newKnowledge.retrievalTest.result": "पुनर्प्राप्ति परिणाम",
|
||||
"newKnowledge.retrievalTest.retrieving": "साक्ष्य प्राप्त करें",
|
||||
"newKnowledge.retrievalTest.retrievingActive": "साक्ष्य प्राप्त करें…",
|
||||
"newKnowledge.retrievalTest.retry": "फिर से कोशिश करें",
|
||||
"newKnowledge.retrievalTest.run": "चलाएँ",
|
||||
"newKnowledge.retrievalTest.running": "रिसर्च जारी है…",
|
||||
|
||||
@ -504,6 +504,7 @@
|
||||
"newKnowledge.retrievalCount": "Jumlah pengambilan",
|
||||
"newKnowledge.retrievalCountValue": "{{value}} dalam 7 hari terakhir",
|
||||
"newKnowledge.retrievalTest.analyzing": "Analisis bukti",
|
||||
"newKnowledge.retrievalTest.analyzingActive": "Analisis bukti…",
|
||||
"newKnowledge.retrievalTest.cancel": "Batal",
|
||||
"newKnowledge.retrievalTest.canceled": "Dibatalkan",
|
||||
"newKnowledge.retrievalTest.completedIn": "Selesai dalam {{duration}}",
|
||||
@ -515,6 +516,7 @@
|
||||
"newKnowledge.retrievalTest.failedTitle": "Pengambilan gagal",
|
||||
"newKnowledge.retrievalTest.foundSoFar": "Ditemukan sejauh ini ({{count}})",
|
||||
"newKnowledge.retrievalTest.generating": "Buat jawaban",
|
||||
"newKnowledge.retrievalTest.generatingActive": "Buat jawaban…",
|
||||
"newKnowledge.retrievalTest.justNow": "Baru saja",
|
||||
"newKnowledge.retrievalTest.keepGoldenQuestion": "Simpan sebagai pertanyaan referensi",
|
||||
"newKnowledge.retrievalTest.makeBadCase": "Simpan sebagai kasus bermasalah",
|
||||
@ -523,13 +525,15 @@
|
||||
"newKnowledge.retrievalTest.open": "Buka",
|
||||
"newKnowledge.retrievalTest.page": "Halaman {{page}}",
|
||||
"newKnowledge.retrievalTest.planning": "Rencanakan riset",
|
||||
"newKnowledge.retrievalTest.planningActive": "Rencanakan riset…",
|
||||
"newKnowledge.retrievalTest.processLog": "Log proses",
|
||||
"newKnowledge.retrievalTest.quality": "Kualitas",
|
||||
"newKnowledge.retrievalTest.queryPlaceholder": "Masukkan kueri untuk menguji pengambilan…",
|
||||
"newKnowledge.retrievalTest.records": "Riwayat",
|
||||
"newKnowledge.retrievalTest.researchResult": "Hasil riset",
|
||||
"newKnowledge.retrievalTest.result": "Hasil {{mode}}",
|
||||
"newKnowledge.retrievalTest.result": "Hasil pengambilan",
|
||||
"newKnowledge.retrievalTest.retrieving": "Ambil bukti",
|
||||
"newKnowledge.retrievalTest.retrievingActive": "Ambil bukti…",
|
||||
"newKnowledge.retrievalTest.retry": "Coba lagi",
|
||||
"newKnowledge.retrievalTest.run": "Jalankan",
|
||||
"newKnowledge.retrievalTest.running": "Sedang melakukan riset…",
|
||||
|
||||
@ -504,6 +504,7 @@
|
||||
"newKnowledge.retrievalCount": "Numero di recuperi",
|
||||
"newKnowledge.retrievalCountValue": "{{value}} negli ultimi 7 giorni",
|
||||
"newKnowledge.retrievalTest.analyzing": "Analizza le evidenze",
|
||||
"newKnowledge.retrievalTest.analyzingActive": "Analizza le evidenze…",
|
||||
"newKnowledge.retrievalTest.cancel": "Annulla",
|
||||
"newKnowledge.retrievalTest.canceled": "Annullato",
|
||||
"newKnowledge.retrievalTest.completedIn": "Completato in {{duration}}",
|
||||
@ -515,6 +516,7 @@
|
||||
"newKnowledge.retrievalTest.failedTitle": "Recupero non riuscito",
|
||||
"newKnowledge.retrievalTest.foundSoFar": "Trovati finora ({{count}})",
|
||||
"newKnowledge.retrievalTest.generating": "Genera risposta",
|
||||
"newKnowledge.retrievalTest.generatingActive": "Genera risposta…",
|
||||
"newKnowledge.retrievalTest.justNow": "Adesso",
|
||||
"newKnowledge.retrievalTest.keepGoldenQuestion": "Conserva come domanda di riferimento",
|
||||
"newKnowledge.retrievalTest.makeBadCase": "Salva come caso problematico",
|
||||
@ -523,13 +525,15 @@
|
||||
"newKnowledge.retrievalTest.open": "Apri",
|
||||
"newKnowledge.retrievalTest.page": "Pagina {{page}}",
|
||||
"newKnowledge.retrievalTest.planning": "Pianifica la ricerca",
|
||||
"newKnowledge.retrievalTest.planningActive": "Pianifica la ricerca…",
|
||||
"newKnowledge.retrievalTest.processLog": "Registro del processo",
|
||||
"newKnowledge.retrievalTest.quality": "Qualità",
|
||||
"newKnowledge.retrievalTest.queryPlaceholder": "Inserisci una query per provare il recupero…",
|
||||
"newKnowledge.retrievalTest.records": "Cronologia",
|
||||
"newKnowledge.retrievalTest.researchResult": "Risultato della ricerca",
|
||||
"newKnowledge.retrievalTest.result": "Risultato {{mode}}",
|
||||
"newKnowledge.retrievalTest.result": "Risultato del recupero",
|
||||
"newKnowledge.retrievalTest.retrieving": "Recupera le evidenze",
|
||||
"newKnowledge.retrievalTest.retrievingActive": "Recupera le evidenze…",
|
||||
"newKnowledge.retrievalTest.retry": "Riprova",
|
||||
"newKnowledge.retrievalTest.run": "Esegui",
|
||||
"newKnowledge.retrievalTest.running": "Ricerca in corso…",
|
||||
|
||||
@ -504,6 +504,7 @@
|
||||
"newKnowledge.retrievalCount": "取得回数",
|
||||
"newKnowledge.retrievalCountValue": "過去7日間で{{value}}回",
|
||||
"newKnowledge.retrievalTest.analyzing": "根拠を分析",
|
||||
"newKnowledge.retrievalTest.analyzingActive": "根拠を分析…",
|
||||
"newKnowledge.retrievalTest.cancel": "キャンセル",
|
||||
"newKnowledge.retrievalTest.canceled": "キャンセル済み",
|
||||
"newKnowledge.retrievalTest.completedIn": "{{duration}} で完了",
|
||||
@ -515,6 +516,7 @@
|
||||
"newKnowledge.retrievalTest.failedTitle": "検索に失敗しました",
|
||||
"newKnowledge.retrievalTest.foundSoFar": "現在までに取得({{count}})",
|
||||
"newKnowledge.retrievalTest.generating": "回答を生成",
|
||||
"newKnowledge.retrievalTest.generatingActive": "回答を生成…",
|
||||
"newKnowledge.retrievalTest.justNow": "たった今",
|
||||
"newKnowledge.retrievalTest.keepGoldenQuestion": "参考質問として保存",
|
||||
"newKnowledge.retrievalTest.makeBadCase": "改善ケースとして保存",
|
||||
@ -523,13 +525,15 @@
|
||||
"newKnowledge.retrievalTest.open": "開く",
|
||||
"newKnowledge.retrievalTest.page": "{{page}} ページ",
|
||||
"newKnowledge.retrievalTest.planning": "調査を計画",
|
||||
"newKnowledge.retrievalTest.planningActive": "調査を計画…",
|
||||
"newKnowledge.retrievalTest.processLog": "プロセスログ",
|
||||
"newKnowledge.retrievalTest.quality": "品質",
|
||||
"newKnowledge.retrievalTest.queryPlaceholder": "検索をテストするクエリを入力…",
|
||||
"newKnowledge.retrievalTest.records": "履歴",
|
||||
"newKnowledge.retrievalTest.researchResult": "調査結果",
|
||||
"newKnowledge.retrievalTest.result": "{{mode}} の結果",
|
||||
"newKnowledge.retrievalTest.result": "検索結果",
|
||||
"newKnowledge.retrievalTest.retrieving": "根拠を取得",
|
||||
"newKnowledge.retrievalTest.retrievingActive": "根拠を取得…",
|
||||
"newKnowledge.retrievalTest.retry": "再試行",
|
||||
"newKnowledge.retrievalTest.run": "実行",
|
||||
"newKnowledge.retrievalTest.running": "調査中…",
|
||||
|
||||
@ -504,6 +504,7 @@
|
||||
"newKnowledge.retrievalCount": "검색 횟수",
|
||||
"newKnowledge.retrievalCountValue": "최근 7일 동안 {{value}}회",
|
||||
"newKnowledge.retrievalTest.analyzing": "근거 분석",
|
||||
"newKnowledge.retrievalTest.analyzingActive": "근거 분석…",
|
||||
"newKnowledge.retrievalTest.cancel": "취소",
|
||||
"newKnowledge.retrievalTest.canceled": "취소됨",
|
||||
"newKnowledge.retrievalTest.completedIn": "{{duration}} 만에 완료",
|
||||
@ -515,6 +516,7 @@
|
||||
"newKnowledge.retrievalTest.failedTitle": "검색 실패",
|
||||
"newKnowledge.retrievalTest.foundSoFar": "현재까지 검색됨({{count}})",
|
||||
"newKnowledge.retrievalTest.generating": "응답 생성",
|
||||
"newKnowledge.retrievalTest.generatingActive": "응답 생성…",
|
||||
"newKnowledge.retrievalTest.justNow": "방금",
|
||||
"newKnowledge.retrievalTest.keepGoldenQuestion": "참고 질문으로 저장",
|
||||
"newKnowledge.retrievalTest.makeBadCase": "개선 사례로 저장",
|
||||
@ -523,13 +525,15 @@
|
||||
"newKnowledge.retrievalTest.open": "열기",
|
||||
"newKnowledge.retrievalTest.page": "{{page}}페이지",
|
||||
"newKnowledge.retrievalTest.planning": "리서치 계획",
|
||||
"newKnowledge.retrievalTest.planningActive": "리서치 계획…",
|
||||
"newKnowledge.retrievalTest.processLog": "프로세스 로그",
|
||||
"newKnowledge.retrievalTest.quality": "품질",
|
||||
"newKnowledge.retrievalTest.queryPlaceholder": "검색을 테스트할 쿼리를 입력하세요…",
|
||||
"newKnowledge.retrievalTest.records": "기록",
|
||||
"newKnowledge.retrievalTest.researchResult": "리서치 결과",
|
||||
"newKnowledge.retrievalTest.result": "{{mode}} 결과",
|
||||
"newKnowledge.retrievalTest.result": "검색 결과",
|
||||
"newKnowledge.retrievalTest.retrieving": "근거 검색",
|
||||
"newKnowledge.retrievalTest.retrievingActive": "근거 검색…",
|
||||
"newKnowledge.retrievalTest.retry": "다시 시도",
|
||||
"newKnowledge.retrievalTest.run": "실행",
|
||||
"newKnowledge.retrievalTest.running": "리서치 중…",
|
||||
|
||||
@ -504,6 +504,7 @@
|
||||
"newKnowledge.retrievalCount": "Aantal zoekacties",
|
||||
"newKnowledge.retrievalCountValue": "{{value}} in de afgelopen 7 dagen",
|
||||
"newKnowledge.retrievalTest.analyzing": "Bewijs analyseren",
|
||||
"newKnowledge.retrievalTest.analyzingActive": "Bewijs analyseren…",
|
||||
"newKnowledge.retrievalTest.cancel": "Annuleren",
|
||||
"newKnowledge.retrievalTest.canceled": "Geannuleerd",
|
||||
"newKnowledge.retrievalTest.completedIn": "Voltooid in {{duration}}",
|
||||
@ -515,6 +516,7 @@
|
||||
"newKnowledge.retrievalTest.failedTitle": "Ophalen mislukt",
|
||||
"newKnowledge.retrievalTest.foundSoFar": "Tot nu toe gevonden ({{count}})",
|
||||
"newKnowledge.retrievalTest.generating": "Antwoord genereren",
|
||||
"newKnowledge.retrievalTest.generatingActive": "Antwoord genereren…",
|
||||
"newKnowledge.retrievalTest.justNow": "Zojuist",
|
||||
"newKnowledge.retrievalTest.keepGoldenQuestion": "Opslaan als referentievraag",
|
||||
"newKnowledge.retrievalTest.makeBadCase": "Opslaan als probleemgeval",
|
||||
@ -523,13 +525,15 @@
|
||||
"newKnowledge.retrievalTest.open": "Openen",
|
||||
"newKnowledge.retrievalTest.page": "Pagina {{page}}",
|
||||
"newKnowledge.retrievalTest.planning": "Onderzoek plannen",
|
||||
"newKnowledge.retrievalTest.planningActive": "Onderzoek plannen…",
|
||||
"newKnowledge.retrievalTest.processLog": "Proceslogboek",
|
||||
"newKnowledge.retrievalTest.quality": "Kwaliteit",
|
||||
"newKnowledge.retrievalTest.queryPlaceholder": "Voer een zoekvraag in om het ophalen te testen…",
|
||||
"newKnowledge.retrievalTest.records": "Geschiedenis",
|
||||
"newKnowledge.retrievalTest.researchResult": "Onderzoeksresultaat",
|
||||
"newKnowledge.retrievalTest.result": "{{mode}}-resultaat",
|
||||
"newKnowledge.retrievalTest.result": "Zoekresultaat",
|
||||
"newKnowledge.retrievalTest.retrieving": "Bewijs ophalen",
|
||||
"newKnowledge.retrievalTest.retrievingActive": "Bewijs ophalen…",
|
||||
"newKnowledge.retrievalTest.retry": "Opnieuw proberen",
|
||||
"newKnowledge.retrievalTest.run": "Uitvoeren",
|
||||
"newKnowledge.retrievalTest.running": "Onderzoek wordt uitgevoerd…",
|
||||
|
||||
@ -504,6 +504,7 @@
|
||||
"newKnowledge.retrievalCount": "Liczba pobrań",
|
||||
"newKnowledge.retrievalCountValue": "{{value}} w ciągu ostatnich 7 dni",
|
||||
"newKnowledge.retrievalTest.analyzing": "Analizuj dowody",
|
||||
"newKnowledge.retrievalTest.analyzingActive": "Analizuj dowody…",
|
||||
"newKnowledge.retrievalTest.cancel": "Anuluj",
|
||||
"newKnowledge.retrievalTest.canceled": "Anulowano",
|
||||
"newKnowledge.retrievalTest.completedIn": "Ukończono w {{duration}}",
|
||||
@ -515,6 +516,7 @@
|
||||
"newKnowledge.retrievalTest.failedTitle": "Wyszukiwanie nie powiodło się",
|
||||
"newKnowledge.retrievalTest.foundSoFar": "Znaleziono dotychczas ({{count}})",
|
||||
"newKnowledge.retrievalTest.generating": "Generuj odpowiedź",
|
||||
"newKnowledge.retrievalTest.generatingActive": "Generuj odpowiedź…",
|
||||
"newKnowledge.retrievalTest.justNow": "Przed chwilą",
|
||||
"newKnowledge.retrievalTest.keepGoldenQuestion": "Zapisz jako pytanie referencyjne",
|
||||
"newKnowledge.retrievalTest.makeBadCase": "Zapisz jako przypadek do poprawy",
|
||||
@ -523,13 +525,15 @@
|
||||
"newKnowledge.retrievalTest.open": "Otwórz",
|
||||
"newKnowledge.retrievalTest.page": "Strona {{page}}",
|
||||
"newKnowledge.retrievalTest.planning": "Zaplanuj badanie",
|
||||
"newKnowledge.retrievalTest.planningActive": "Zaplanuj badanie…",
|
||||
"newKnowledge.retrievalTest.processLog": "Dziennik procesu",
|
||||
"newKnowledge.retrievalTest.quality": "Jakość",
|
||||
"newKnowledge.retrievalTest.queryPlaceholder": "Wprowadź zapytanie, aby przetestować wyszukiwanie…",
|
||||
"newKnowledge.retrievalTest.records": "Historia",
|
||||
"newKnowledge.retrievalTest.researchResult": "Wynik badania",
|
||||
"newKnowledge.retrievalTest.result": "Wynik: {{mode}}",
|
||||
"newKnowledge.retrievalTest.result": "Wynik wyszukiwania",
|
||||
"newKnowledge.retrievalTest.retrieving": "Pobierz dowody",
|
||||
"newKnowledge.retrievalTest.retrievingActive": "Pobierz dowody…",
|
||||
"newKnowledge.retrievalTest.retry": "Spróbuj ponownie",
|
||||
"newKnowledge.retrievalTest.run": "Uruchom",
|
||||
"newKnowledge.retrievalTest.running": "Trwa badanie…",
|
||||
|
||||
@ -504,6 +504,7 @@
|
||||
"newKnowledge.retrievalCount": "Quantidade de recuperações",
|
||||
"newKnowledge.retrievalCountValue": "{{value}} nos últimos 7 dias",
|
||||
"newKnowledge.retrievalTest.analyzing": "Analisar evidências",
|
||||
"newKnowledge.retrievalTest.analyzingActive": "Analisar evidências…",
|
||||
"newKnowledge.retrievalTest.cancel": "Cancelar",
|
||||
"newKnowledge.retrievalTest.canceled": "Cancelado",
|
||||
"newKnowledge.retrievalTest.completedIn": "Concluído em {{duration}}",
|
||||
@ -515,6 +516,7 @@
|
||||
"newKnowledge.retrievalTest.failedTitle": "Falha na recuperação",
|
||||
"newKnowledge.retrievalTest.foundSoFar": "Encontrados até agora ({{count}})",
|
||||
"newKnowledge.retrievalTest.generating": "Gerar resposta",
|
||||
"newKnowledge.retrievalTest.generatingActive": "Gerar resposta…",
|
||||
"newKnowledge.retrievalTest.justNow": "Agora mesmo",
|
||||
"newKnowledge.retrievalTest.keepGoldenQuestion": "Salvar como pergunta de referência",
|
||||
"newKnowledge.retrievalTest.makeBadCase": "Salvar como caso problemático",
|
||||
@ -523,13 +525,15 @@
|
||||
"newKnowledge.retrievalTest.open": "Abrir",
|
||||
"newKnowledge.retrievalTest.page": "Página {{page}}",
|
||||
"newKnowledge.retrievalTest.planning": "Planejar pesquisa",
|
||||
"newKnowledge.retrievalTest.planningActive": "Planejar pesquisa…",
|
||||
"newKnowledge.retrievalTest.processLog": "Log do processo",
|
||||
"newKnowledge.retrievalTest.quality": "Qualidade",
|
||||
"newKnowledge.retrievalTest.queryPlaceholder": "Digite uma consulta para testar a recuperação…",
|
||||
"newKnowledge.retrievalTest.records": "Histórico",
|
||||
"newKnowledge.retrievalTest.researchResult": "Resultado da pesquisa",
|
||||
"newKnowledge.retrievalTest.result": "Resultado de {{mode}}",
|
||||
"newKnowledge.retrievalTest.result": "Resultado da recuperação",
|
||||
"newKnowledge.retrievalTest.retrieving": "Recuperar evidências",
|
||||
"newKnowledge.retrievalTest.retrievingActive": "Recuperar evidências…",
|
||||
"newKnowledge.retrievalTest.retry": "Tentar novamente",
|
||||
"newKnowledge.retrievalTest.run": "Executar",
|
||||
"newKnowledge.retrievalTest.running": "Pesquisando…",
|
||||
|
||||
@ -504,6 +504,7 @@
|
||||
"newKnowledge.retrievalCount": "Număr de regăsiri",
|
||||
"newKnowledge.retrievalCountValue": "{{value}} în ultimele 7 zile",
|
||||
"newKnowledge.retrievalTest.analyzing": "Analizează dovezile",
|
||||
"newKnowledge.retrievalTest.analyzingActive": "Analizează dovezile…",
|
||||
"newKnowledge.retrievalTest.cancel": "Anulează",
|
||||
"newKnowledge.retrievalTest.canceled": "Anulat",
|
||||
"newKnowledge.retrievalTest.completedIn": "Finalizat în {{duration}}",
|
||||
@ -515,6 +516,7 @@
|
||||
"newKnowledge.retrievalTest.failedTitle": "Recuperarea a eșuat",
|
||||
"newKnowledge.retrievalTest.foundSoFar": "Găsite până acum ({{count}})",
|
||||
"newKnowledge.retrievalTest.generating": "Generează răspunsul",
|
||||
"newKnowledge.retrievalTest.generatingActive": "Generează răspunsul…",
|
||||
"newKnowledge.retrievalTest.justNow": "Chiar acum",
|
||||
"newKnowledge.retrievalTest.keepGoldenQuestion": "Salvează ca întrebare de referință",
|
||||
"newKnowledge.retrievalTest.makeBadCase": "Salvează ca situație problematică",
|
||||
@ -523,13 +525,15 @@
|
||||
"newKnowledge.retrievalTest.open": "Deschide",
|
||||
"newKnowledge.retrievalTest.page": "Pagina {{page}}",
|
||||
"newKnowledge.retrievalTest.planning": "Planifică cercetarea",
|
||||
"newKnowledge.retrievalTest.planningActive": "Planifică cercetarea…",
|
||||
"newKnowledge.retrievalTest.processLog": "Jurnalul procesului",
|
||||
"newKnowledge.retrievalTest.quality": "Calitate",
|
||||
"newKnowledge.retrievalTest.queryPlaceholder": "Introduceți o interogare pentru a testa recuperarea…",
|
||||
"newKnowledge.retrievalTest.records": "Istoric",
|
||||
"newKnowledge.retrievalTest.researchResult": "Rezultatul cercetării",
|
||||
"newKnowledge.retrievalTest.result": "Rezultat {{mode}}",
|
||||
"newKnowledge.retrievalTest.result": "Rezultatul regăsirii",
|
||||
"newKnowledge.retrievalTest.retrieving": "Recuperează dovezile",
|
||||
"newKnowledge.retrievalTest.retrievingActive": "Recuperează dovezile…",
|
||||
"newKnowledge.retrievalTest.retry": "Reîncearcă",
|
||||
"newKnowledge.retrievalTest.run": "Rulează",
|
||||
"newKnowledge.retrievalTest.running": "Cercetare în curs…",
|
||||
|
||||
@ -504,6 +504,7 @@
|
||||
"newKnowledge.retrievalCount": "Количество извлечений",
|
||||
"newKnowledge.retrievalCountValue": "{{value}} за последние 7 дней",
|
||||
"newKnowledge.retrievalTest.analyzing": "Анализ доказательств",
|
||||
"newKnowledge.retrievalTest.analyzingActive": "Анализ доказательств…",
|
||||
"newKnowledge.retrievalTest.cancel": "Отмена",
|
||||
"newKnowledge.retrievalTest.canceled": "Отменено",
|
||||
"newKnowledge.retrievalTest.completedIn": "Завершено за {{duration}}",
|
||||
@ -515,6 +516,7 @@
|
||||
"newKnowledge.retrievalTest.failedTitle": "Ошибка поиска",
|
||||
"newKnowledge.retrievalTest.foundSoFar": "Найдено на данный момент ({{count}})",
|
||||
"newKnowledge.retrievalTest.generating": "Создание ответа",
|
||||
"newKnowledge.retrievalTest.generatingActive": "Создание ответа…",
|
||||
"newKnowledge.retrievalTest.justNow": "Только что",
|
||||
"newKnowledge.retrievalTest.keepGoldenQuestion": "Сохранить как эталонный вопрос",
|
||||
"newKnowledge.retrievalTest.makeBadCase": "Сохранить как проблемный случай",
|
||||
@ -523,13 +525,15 @@
|
||||
"newKnowledge.retrievalTest.open": "Открыть",
|
||||
"newKnowledge.retrievalTest.page": "Страница {{page}}",
|
||||
"newKnowledge.retrievalTest.planning": "Планирование исследования",
|
||||
"newKnowledge.retrievalTest.planningActive": "Планирование исследования…",
|
||||
"newKnowledge.retrievalTest.processLog": "Журнал процесса",
|
||||
"newKnowledge.retrievalTest.quality": "Качество",
|
||||
"newKnowledge.retrievalTest.queryPlaceholder": "Введите запрос для проверки поиска…",
|
||||
"newKnowledge.retrievalTest.records": "История",
|
||||
"newKnowledge.retrievalTest.researchResult": "Результат исследования",
|
||||
"newKnowledge.retrievalTest.result": "Результат: {{mode}}",
|
||||
"newKnowledge.retrievalTest.result": "Результат поиска",
|
||||
"newKnowledge.retrievalTest.retrieving": "Поиск доказательств",
|
||||
"newKnowledge.retrievalTest.retrievingActive": "Поиск доказательств…",
|
||||
"newKnowledge.retrievalTest.retry": "Повторить",
|
||||
"newKnowledge.retrievalTest.run": "Запустить",
|
||||
"newKnowledge.retrievalTest.running": "Идёт исследование…",
|
||||
|
||||
@ -504,6 +504,7 @@
|
||||
"newKnowledge.retrievalCount": "Število pridobitev",
|
||||
"newKnowledge.retrievalCountValue": "{{value}} v zadnjih 7 dneh",
|
||||
"newKnowledge.retrievalTest.analyzing": "Analiziraj dokaze",
|
||||
"newKnowledge.retrievalTest.analyzingActive": "Analiziraj dokaze…",
|
||||
"newKnowledge.retrievalTest.cancel": "Prekliči",
|
||||
"newKnowledge.retrievalTest.canceled": "Preklicano",
|
||||
"newKnowledge.retrievalTest.completedIn": "Končano v {{duration}}",
|
||||
@ -515,6 +516,7 @@
|
||||
"newKnowledge.retrievalTest.failedTitle": "Pridobivanje ni uspelo",
|
||||
"newKnowledge.retrievalTest.foundSoFar": "Doslej najdeno ({{count}})",
|
||||
"newKnowledge.retrievalTest.generating": "Ustvari odgovor",
|
||||
"newKnowledge.retrievalTest.generatingActive": "Ustvari odgovor…",
|
||||
"newKnowledge.retrievalTest.justNow": "Pravkar",
|
||||
"newKnowledge.retrievalTest.keepGoldenQuestion": "Shrani kot referenčno vprašanje",
|
||||
"newKnowledge.retrievalTest.makeBadCase": "Shrani kot problematičen primer",
|
||||
@ -523,13 +525,15 @@
|
||||
"newKnowledge.retrievalTest.open": "Odpri",
|
||||
"newKnowledge.retrievalTest.page": "Stran {{page}}",
|
||||
"newKnowledge.retrievalTest.planning": "Načrtuj raziskavo",
|
||||
"newKnowledge.retrievalTest.planningActive": "Načrtuj raziskavo…",
|
||||
"newKnowledge.retrievalTest.processLog": "Dnevnik postopka",
|
||||
"newKnowledge.retrievalTest.quality": "Kakovost",
|
||||
"newKnowledge.retrievalTest.queryPlaceholder": "Vnesite poizvedbo za preskus pridobivanja…",
|
||||
"newKnowledge.retrievalTest.records": "Zgodovina",
|
||||
"newKnowledge.retrievalTest.researchResult": "Rezultat raziskave",
|
||||
"newKnowledge.retrievalTest.result": "Rezultat {{mode}}",
|
||||
"newKnowledge.retrievalTest.result": "Rezultat iskanja",
|
||||
"newKnowledge.retrievalTest.retrieving": "Pridobi dokaze",
|
||||
"newKnowledge.retrievalTest.retrievingActive": "Pridobi dokaze…",
|
||||
"newKnowledge.retrievalTest.retry": "Poskusi znova",
|
||||
"newKnowledge.retrievalTest.run": "Zaženi",
|
||||
"newKnowledge.retrievalTest.running": "Raziskovanje…",
|
||||
|
||||
@ -504,6 +504,7 @@
|
||||
"newKnowledge.retrievalCount": "จำนวนการเรียกค้น",
|
||||
"newKnowledge.retrievalCountValue": "{{value}} ครั้งในช่วง 7 วันที่ผ่านมา",
|
||||
"newKnowledge.retrievalTest.analyzing": "วิเคราะห์หลักฐาน",
|
||||
"newKnowledge.retrievalTest.analyzingActive": "วิเคราะห์หลักฐาน…",
|
||||
"newKnowledge.retrievalTest.cancel": "ยกเลิก",
|
||||
"newKnowledge.retrievalTest.canceled": "ยกเลิกแล้ว",
|
||||
"newKnowledge.retrievalTest.completedIn": "เสร็จสิ้นใน {{duration}}",
|
||||
@ -515,6 +516,7 @@
|
||||
"newKnowledge.retrievalTest.failedTitle": "การค้นคืนล้มเหลว",
|
||||
"newKnowledge.retrievalTest.foundSoFar": "พบแล้ว ({{count}})",
|
||||
"newKnowledge.retrievalTest.generating": "สร้างคำตอบ",
|
||||
"newKnowledge.retrievalTest.generatingActive": "สร้างคำตอบ…",
|
||||
"newKnowledge.retrievalTest.justNow": "เมื่อสักครู่",
|
||||
"newKnowledge.retrievalTest.keepGoldenQuestion": "บันทึกเป็นคำถามอ้างอิง",
|
||||
"newKnowledge.retrievalTest.makeBadCase": "บันทึกเป็นกรณีที่ต้องปรับปรุง",
|
||||
@ -523,13 +525,15 @@
|
||||
"newKnowledge.retrievalTest.open": "เปิด",
|
||||
"newKnowledge.retrievalTest.page": "หน้า {{page}}",
|
||||
"newKnowledge.retrievalTest.planning": "วางแผนการวิจัย",
|
||||
"newKnowledge.retrievalTest.planningActive": "วางแผนการวิจัย…",
|
||||
"newKnowledge.retrievalTest.processLog": "บันทึกกระบวนการ",
|
||||
"newKnowledge.retrievalTest.quality": "คุณภาพ",
|
||||
"newKnowledge.retrievalTest.queryPlaceholder": "ป้อนคำค้นเพื่อทดสอบการค้นคืน…",
|
||||
"newKnowledge.retrievalTest.records": "ประวัติ",
|
||||
"newKnowledge.retrievalTest.researchResult": "ผลการวิจัย",
|
||||
"newKnowledge.retrievalTest.result": "ผลลัพธ์ {{mode}}",
|
||||
"newKnowledge.retrievalTest.result": "ผลลัพธ์การดึงข้อมูล",
|
||||
"newKnowledge.retrievalTest.retrieving": "ค้นคืนหลักฐาน",
|
||||
"newKnowledge.retrievalTest.retrievingActive": "ค้นคืนหลักฐาน…",
|
||||
"newKnowledge.retrievalTest.retry": "ลองอีกครั้ง",
|
||||
"newKnowledge.retrievalTest.run": "เรียกใช้",
|
||||
"newKnowledge.retrievalTest.running": "กำลังวิจัย…",
|
||||
|
||||
@ -504,6 +504,7 @@
|
||||
"newKnowledge.retrievalCount": "Getirme sayısı",
|
||||
"newKnowledge.retrievalCountValue": "Son 7 günde {{value}}",
|
||||
"newKnowledge.retrievalTest.analyzing": "Kanıtları analiz et",
|
||||
"newKnowledge.retrievalTest.analyzingActive": "Kanıtları analiz et…",
|
||||
"newKnowledge.retrievalTest.cancel": "İptal",
|
||||
"newKnowledge.retrievalTest.canceled": "İptal edildi",
|
||||
"newKnowledge.retrievalTest.completedIn": "{{duration}} içinde tamamlandı",
|
||||
@ -515,6 +516,7 @@
|
||||
"newKnowledge.retrievalTest.failedTitle": "Getirme başarısız",
|
||||
"newKnowledge.retrievalTest.foundSoFar": "Şimdiye kadar bulunan ({{count}})",
|
||||
"newKnowledge.retrievalTest.generating": "Yanıt oluştur",
|
||||
"newKnowledge.retrievalTest.generatingActive": "Yanıt oluştur…",
|
||||
"newKnowledge.retrievalTest.justNow": "Az önce",
|
||||
"newKnowledge.retrievalTest.keepGoldenQuestion": "Referans sorusu olarak kaydet",
|
||||
"newKnowledge.retrievalTest.makeBadCase": "Sorunlu durum olarak kaydet",
|
||||
@ -523,13 +525,15 @@
|
||||
"newKnowledge.retrievalTest.open": "Aç",
|
||||
"newKnowledge.retrievalTest.page": "Sayfa {{page}}",
|
||||
"newKnowledge.retrievalTest.planning": "Araştırmayı planla",
|
||||
"newKnowledge.retrievalTest.planningActive": "Araştırmayı planla…",
|
||||
"newKnowledge.retrievalTest.processLog": "İşlem günlüğü",
|
||||
"newKnowledge.retrievalTest.quality": "Kalite",
|
||||
"newKnowledge.retrievalTest.queryPlaceholder": "Getirmeyi test etmek için bir sorgu girin…",
|
||||
"newKnowledge.retrievalTest.records": "Geçmiş",
|
||||
"newKnowledge.retrievalTest.researchResult": "Araştırma sonucu",
|
||||
"newKnowledge.retrievalTest.result": "{{mode}} sonucu",
|
||||
"newKnowledge.retrievalTest.result": "Getirme sonucu",
|
||||
"newKnowledge.retrievalTest.retrieving": "Kanıtları getir",
|
||||
"newKnowledge.retrievalTest.retrievingActive": "Kanıtları getir…",
|
||||
"newKnowledge.retrievalTest.retry": "Tekrar dene",
|
||||
"newKnowledge.retrievalTest.run": "Çalıştır",
|
||||
"newKnowledge.retrievalTest.running": "Araştırılıyor…",
|
||||
|
||||
@ -504,6 +504,7 @@
|
||||
"newKnowledge.retrievalCount": "Кількість отримань",
|
||||
"newKnowledge.retrievalCountValue": "{{value}} за останні 7 днів",
|
||||
"newKnowledge.retrievalTest.analyzing": "Аналіз доказів",
|
||||
"newKnowledge.retrievalTest.analyzingActive": "Аналіз доказів…",
|
||||
"newKnowledge.retrievalTest.cancel": "Скасувати",
|
||||
"newKnowledge.retrievalTest.canceled": "Скасовано",
|
||||
"newKnowledge.retrievalTest.completedIn": "Завершено за {{duration}}",
|
||||
@ -515,6 +516,7 @@
|
||||
"newKnowledge.retrievalTest.failedTitle": "Помилка пошуку",
|
||||
"newKnowledge.retrievalTest.foundSoFar": "Знайдено наразі ({{count}})",
|
||||
"newKnowledge.retrievalTest.generating": "Створення відповіді",
|
||||
"newKnowledge.retrievalTest.generatingActive": "Створення відповіді…",
|
||||
"newKnowledge.retrievalTest.justNow": "Щойно",
|
||||
"newKnowledge.retrievalTest.keepGoldenQuestion": "Зберегти як еталонне запитання",
|
||||
"newKnowledge.retrievalTest.makeBadCase": "Зберегти як проблемний випадок",
|
||||
@ -523,13 +525,15 @@
|
||||
"newKnowledge.retrievalTest.open": "Відкрити",
|
||||
"newKnowledge.retrievalTest.page": "Сторінка {{page}}",
|
||||
"newKnowledge.retrievalTest.planning": "Планування дослідження",
|
||||
"newKnowledge.retrievalTest.planningActive": "Планування дослідження…",
|
||||
"newKnowledge.retrievalTest.processLog": "Журнал процесу",
|
||||
"newKnowledge.retrievalTest.quality": "Якість",
|
||||
"newKnowledge.retrievalTest.queryPlaceholder": "Введіть запит, щоб перевірити пошук…",
|
||||
"newKnowledge.retrievalTest.records": "Історія",
|
||||
"newKnowledge.retrievalTest.researchResult": "Результат дослідження",
|
||||
"newKnowledge.retrievalTest.result": "Результат: {{mode}}",
|
||||
"newKnowledge.retrievalTest.result": "Результат пошуку",
|
||||
"newKnowledge.retrievalTest.retrieving": "Пошук доказів",
|
||||
"newKnowledge.retrievalTest.retrievingActive": "Пошук доказів…",
|
||||
"newKnowledge.retrievalTest.retry": "Повторити",
|
||||
"newKnowledge.retrievalTest.run": "Запустити",
|
||||
"newKnowledge.retrievalTest.running": "Триває дослідження…",
|
||||
|
||||
@ -504,6 +504,7 @@
|
||||
"newKnowledge.retrievalCount": "Số lần truy xuất",
|
||||
"newKnowledge.retrievalCountValue": "{{value}} trong 7 ngày qua",
|
||||
"newKnowledge.retrievalTest.analyzing": "Phân tích bằng chứng",
|
||||
"newKnowledge.retrievalTest.analyzingActive": "Phân tích bằng chứng…",
|
||||
"newKnowledge.retrievalTest.cancel": "Hủy",
|
||||
"newKnowledge.retrievalTest.canceled": "Đã hủy",
|
||||
"newKnowledge.retrievalTest.completedIn": "Hoàn tất trong {{duration}}",
|
||||
@ -515,6 +516,7 @@
|
||||
"newKnowledge.retrievalTest.failedTitle": "Truy xuất thất bại",
|
||||
"newKnowledge.retrievalTest.foundSoFar": "Đã tìm thấy ({{count}})",
|
||||
"newKnowledge.retrievalTest.generating": "Tạo câu trả lời",
|
||||
"newKnowledge.retrievalTest.generatingActive": "Tạo câu trả lời…",
|
||||
"newKnowledge.retrievalTest.justNow": "Vừa xong",
|
||||
"newKnowledge.retrievalTest.keepGoldenQuestion": "Lưu làm câu hỏi tham chiếu",
|
||||
"newKnowledge.retrievalTest.makeBadCase": "Lưu làm trường hợp cần cải thiện",
|
||||
@ -523,13 +525,15 @@
|
||||
"newKnowledge.retrievalTest.open": "Mở",
|
||||
"newKnowledge.retrievalTest.page": "Trang {{page}}",
|
||||
"newKnowledge.retrievalTest.planning": "Lập kế hoạch nghiên cứu",
|
||||
"newKnowledge.retrievalTest.planningActive": "Lập kế hoạch nghiên cứu…",
|
||||
"newKnowledge.retrievalTest.processLog": "Nhật ký quy trình",
|
||||
"newKnowledge.retrievalTest.quality": "Chất lượng",
|
||||
"newKnowledge.retrievalTest.queryPlaceholder": "Nhập truy vấn để kiểm thử truy xuất…",
|
||||
"newKnowledge.retrievalTest.records": "Lịch sử",
|
||||
"newKnowledge.retrievalTest.researchResult": "Kết quả nghiên cứu",
|
||||
"newKnowledge.retrievalTest.result": "Kết quả {{mode}}",
|
||||
"newKnowledge.retrievalTest.result": "Kết quả truy xuất",
|
||||
"newKnowledge.retrievalTest.retrieving": "Truy xuất bằng chứng",
|
||||
"newKnowledge.retrievalTest.retrievingActive": "Truy xuất bằng chứng…",
|
||||
"newKnowledge.retrievalTest.retry": "Thử lại",
|
||||
"newKnowledge.retrievalTest.run": "Chạy",
|
||||
"newKnowledge.retrievalTest.running": "Đang nghiên cứu…",
|
||||
|
||||
@ -504,6 +504,7 @@
|
||||
"newKnowledge.retrievalCount": "检索次数",
|
||||
"newKnowledge.retrievalCountValue": "过去 7 天内 {{value}} 次",
|
||||
"newKnowledge.retrievalTest.analyzing": "分析证据",
|
||||
"newKnowledge.retrievalTest.analyzingActive": "分析证据…",
|
||||
"newKnowledge.retrievalTest.cancel": "取消",
|
||||
"newKnowledge.retrievalTest.canceled": "已取消",
|
||||
"newKnowledge.retrievalTest.completedIn": "已在 {{duration}} 内完成",
|
||||
@ -515,6 +516,7 @@
|
||||
"newKnowledge.retrievalTest.failedTitle": "检索失败",
|
||||
"newKnowledge.retrievalTest.foundSoFar": "目前找到({{count}})",
|
||||
"newKnowledge.retrievalTest.generating": "生成回答",
|
||||
"newKnowledge.retrievalTest.generatingActive": "生成回答…",
|
||||
"newKnowledge.retrievalTest.justNow": "刚刚",
|
||||
"newKnowledge.retrievalTest.keepGoldenQuestion": "保留为参考题",
|
||||
"newKnowledge.retrievalTest.makeBadCase": "标记为不良案例",
|
||||
@ -523,13 +525,15 @@
|
||||
"newKnowledge.retrievalTest.open": "打开",
|
||||
"newKnowledge.retrievalTest.page": "第 {{page}} 页",
|
||||
"newKnowledge.retrievalTest.planning": "规划研究",
|
||||
"newKnowledge.retrievalTest.planningActive": "规划研究…",
|
||||
"newKnowledge.retrievalTest.processLog": "过程日志",
|
||||
"newKnowledge.retrievalTest.quality": "质量",
|
||||
"newKnowledge.retrievalTest.queryPlaceholder": "输入查询以测试检索…",
|
||||
"newKnowledge.retrievalTest.records": "记录",
|
||||
"newKnowledge.retrievalTest.researchResult": "研究结果",
|
||||
"newKnowledge.retrievalTest.result": "{{mode}} 结果",
|
||||
"newKnowledge.retrievalTest.result": "检索结果",
|
||||
"newKnowledge.retrievalTest.retrieving": "检索证据",
|
||||
"newKnowledge.retrievalTest.retrievingActive": "检索证据…",
|
||||
"newKnowledge.retrievalTest.retry": "重试",
|
||||
"newKnowledge.retrievalTest.run": "运行",
|
||||
"newKnowledge.retrievalTest.running": "研究中…",
|
||||
|
||||
@ -504,6 +504,7 @@
|
||||
"newKnowledge.retrievalCount": "檢索次數",
|
||||
"newKnowledge.retrievalCountValue": "過去 7 天內 {{value}} 次",
|
||||
"newKnowledge.retrievalTest.analyzing": "分析證據",
|
||||
"newKnowledge.retrievalTest.analyzingActive": "分析證據…",
|
||||
"newKnowledge.retrievalTest.cancel": "取消",
|
||||
"newKnowledge.retrievalTest.canceled": "已取消",
|
||||
"newKnowledge.retrievalTest.completedIn": "已在 {{duration}} 內完成",
|
||||
@ -515,6 +516,7 @@
|
||||
"newKnowledge.retrievalTest.failedTitle": "檢索失敗",
|
||||
"newKnowledge.retrievalTest.foundSoFar": "目前找到({{count}})",
|
||||
"newKnowledge.retrievalTest.generating": "產生回答",
|
||||
"newKnowledge.retrievalTest.generatingActive": "產生回答…",
|
||||
"newKnowledge.retrievalTest.justNow": "剛剛",
|
||||
"newKnowledge.retrievalTest.keepGoldenQuestion": "保留為參考題",
|
||||
"newKnowledge.retrievalTest.makeBadCase": "標記為不良案例",
|
||||
@ -523,13 +525,15 @@
|
||||
"newKnowledge.retrievalTest.open": "開啟",
|
||||
"newKnowledge.retrievalTest.page": "第 {{page}} 頁",
|
||||
"newKnowledge.retrievalTest.planning": "規劃研究",
|
||||
"newKnowledge.retrievalTest.planningActive": "規劃研究…",
|
||||
"newKnowledge.retrievalTest.processLog": "流程記錄",
|
||||
"newKnowledge.retrievalTest.quality": "品質",
|
||||
"newKnowledge.retrievalTest.queryPlaceholder": "輸入查詢以測試檢索…",
|
||||
"newKnowledge.retrievalTest.records": "記錄",
|
||||
"newKnowledge.retrievalTest.researchResult": "研究結果",
|
||||
"newKnowledge.retrievalTest.result": "{{mode}} 結果",
|
||||
"newKnowledge.retrievalTest.result": "檢索結果",
|
||||
"newKnowledge.retrievalTest.retrieving": "取回證據",
|
||||
"newKnowledge.retrievalTest.retrievingActive": "取回證據…",
|
||||
"newKnowledge.retrievalTest.retry": "重試",
|
||||
"newKnowledge.retrievalTest.run": "執行",
|
||||
"newKnowledge.retrievalTest.running": "研究中…",
|
||||
|
||||
8
web/public/images/new-rag/vibe-coding-star.svg
Normal file
8
web/public/images/new-rag/vibe-coding-star.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="12.6248" height="11.0833" viewBox="0 0 12.6248 11.0833" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Vector">
|
||||
<path d="M10.4911 0.252525C10.4748 0.108798 10.3533 0.00014598 10.2086 1.46976e-07C10.064 -0.000145686 9.94222 0.108249 9.92565 0.251948C9.82217 1.14913 9.31578 1.65551 8.41861 1.75899C8.27493 1.77556 8.16649 1.89731 8.16667 2.04196C8.16684 2.18661 8.27546 2.30813 8.41919 2.32441C9.30364 2.4246 9.84515 2.9261 9.92489 3.82396C9.93796 3.9709 10.0612 4.0835 10.2086 4.08333C10.3562 4.08316 10.4791 3.97029 10.4918 3.82332C10.5684 2.93815 11.1048 2.40178 11.99 2.32516C12.137 2.31244 12.2498 2.18951 12.25 2.04199C12.2502 1.89447 12.1376 1.77128 11.9907 1.75823C11.0928 1.67847 10.5912 1.13698 10.4911 0.252525Z" fill="#354052"/>
|
||||
<path d="M5.97917 3.20833C5.97917 2.40292 5.32625 1.75 4.52083 1.75C3.71542 1.75 3.0625 2.40292 3.0625 3.20833C3.0625 4.01375 3.71542 4.66667 4.52083 4.66667C5.32625 4.66667 5.97917 4.01375 5.97917 3.20833ZM7.14583 3.20833C7.14583 4.65808 5.97058 5.83333 4.52083 5.83333C3.07109 5.83333 1.89583 4.65808 1.89583 3.20833C1.89583 1.75859 3.07109 0.583333 4.52083 0.583333C5.97058 0.583333 7.14583 1.75859 7.14583 3.20833Z" fill="#354052"/>
|
||||
<path d="M0 10.5C0 8.24484 1.82817 6.41667 4.08333 6.41667H4.375C4.69717 6.41667 4.95833 6.67783 4.95833 7C4.95833 7.32217 4.69717 7.58333 4.375 7.58333H4.08333C2.4725 7.58333 1.16667 8.88917 1.16667 10.5C1.16667 10.8222 0.9055 11.0833 0.583333 11.0833C0.261167 11.0833 0 10.8222 0 10.5Z" fill="#354052"/>
|
||||
<path d="M11.3995 5.83333C12.2133 5.83344 12.8005 6.61236 12.577 7.39478L11.7652 10.2374C11.6221 10.7381 11.1643 11.0832 10.6436 11.0833H3.5C3.17783 11.0833 2.91667 10.8222 2.91667 10.5C2.91667 10.1778 3.17783 9.91667 3.5 9.91667H5.39299L6.25773 6.89119C6.43661 6.26521 7.00904 5.83333 7.66024 5.83333H11.3995ZM7.66024 7C7.53005 7 7.41523 7.08619 7.3794 7.21134L6.60693 9.91667H10.6436L11.4553 7.07463C11.466 7.03734 11.4381 7.00011 11.3995 7H7.66024Z" fill="#354052"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
Loading…
Reference in New Issue
Block a user