feat(translate): language detection, async HTTP LLM, history model, agent improvements
- Add async HTTP-based LLM transport (_llm_async_http.py) - Add orthogonal LLM call tests - Improve language detection (_lang_detect.py) and batch insert - Update translate schemas, service utils, preview constants/prompts - Add TranslateHistoryModel with pagination and filtering - Update agent confirmation, persistence, langgraph setup, run, tools - Improve LLM health checking in shared module - Update translate runs API, history route
This commit is contained in:
@@ -272,7 +272,7 @@ describe('fetchAllRuns', () => {
|
||||
expect(mockApi.fetchApi).toHaveBeenCalledWith('/translate/runs');
|
||||
});
|
||||
|
||||
it('fetches all runs with job_id, status, trigger_type, and created_by filters', async () => {
|
||||
it('fetches all runs with job_id, status, trigger_type, created_by, and date filters', async () => {
|
||||
mockApi.fetchApi.mockResolvedValue({ runs: [{ id: 'r1' }] });
|
||||
const { fetchAllRuns } = await import('$lib/api/translate/runs.js');
|
||||
const result = await fetchAllRuns({
|
||||
@@ -280,12 +280,14 @@ describe('fetchAllRuns', () => {
|
||||
status: 'FAILED',
|
||||
trigger_type: 'manual',
|
||||
created_by: 'user1',
|
||||
date_from: '2026-07-01',
|
||||
date_to: '2026-07-08',
|
||||
page: 1,
|
||||
page_size: 25,
|
||||
});
|
||||
expect(result).toEqual({ runs: [{ id: 'r1' }] });
|
||||
expect(mockApi.fetchApi).toHaveBeenCalledWith(
|
||||
'/translate/runs?page=1&page_size=25&job_id=job-1&status=FAILED&trigger_type=manual&created_by=user1',
|
||||
'/translate/runs?page=1&page_size=25&job_id=job-1&status=FAILED&trigger_type=manual&created_by=user1&date_from=2026-07-01&date_to=2026-07-08',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ export interface AllRunsQueryOptions extends RunQueryOptions {
|
||||
status?: string;
|
||||
trigger_type?: string;
|
||||
created_by?: string;
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
}
|
||||
|
||||
// #region triggerRun [C:2] [TYPE Function] [SEMANTICS translate, runs, trigger]
|
||||
@@ -171,6 +173,8 @@ export async function fetchAllRuns<T = unknown>(options: AllRunsQueryOptions = {
|
||||
if (options.status) params.append('status', options.status);
|
||||
if (options.trigger_type) params.append('trigger_type', options.trigger_type);
|
||||
if (options.created_by) params.append('created_by', options.created_by);
|
||||
if (options.date_from) params.append('date_from', options.date_from);
|
||||
if (options.date_to) params.append('date_to', options.date_to);
|
||||
const query = params.toString();
|
||||
return await api.fetchApi<T>(`/translate/runs${query ? `?${query}` : ''}`);
|
||||
} catch (error) {
|
||||
|
||||
@@ -54,7 +54,15 @@ export class TranslateHistoryModel {
|
||||
this.isLoading = true;
|
||||
this.uxState = 'loading';
|
||||
try {
|
||||
const result = await fetchAllRuns({ page: this.currentPage, page_size: this.pageSize, job_id: this.filterJobId || undefined, status: this.filterStatus || undefined, trigger_type: this.filterTrigger || undefined });
|
||||
const result = await fetchAllRuns({
|
||||
page: this.currentPage,
|
||||
page_size: this.pageSize,
|
||||
job_id: this.filterJobId || undefined,
|
||||
status: this.filterStatus || undefined,
|
||||
trigger_type: this.filterTrigger || undefined,
|
||||
date_from: this.filterDateFrom || undefined,
|
||||
date_to: this.filterDateTo || undefined,
|
||||
});
|
||||
this.runs = (result as { items?: Record<string, unknown>[] })?.items || [];
|
||||
this.total = (result as { total?: number })?.total || 0;
|
||||
this.uxState = this.runs.length === 0 ? 'empty' : 'populated';
|
||||
@@ -72,8 +80,20 @@ export class TranslateHistoryModel {
|
||||
|
||||
async loadJobs(): Promise<void> {
|
||||
try {
|
||||
const result = await fetchJobs({ page_size: 100 });
|
||||
this.jobs = Array.isArray(result) ? result : ((result as { items?: unknown[]; results?: unknown[] })?.items || (result as { items?: unknown[]; results?: unknown[] })?.results || []) as Record<string, unknown>[];
|
||||
const pageSize = 100;
|
||||
const first = await fetchJobs({ page: 1, page_size: pageSize });
|
||||
const firstJobs = this._extractJobs(first);
|
||||
const total = Array.isArray(first) ? firstJobs.length : ((first as { total?: number })?.total || firstJobs.length);
|
||||
const jobs = [...firstJobs];
|
||||
|
||||
for (let page = 2; jobs.length < total; page += 1) {
|
||||
const result = await fetchJobs({ page, page_size: pageSize });
|
||||
const pageJobs = this._extractJobs(result);
|
||||
if (pageJobs.length === 0) break;
|
||||
jobs.push(...pageJobs);
|
||||
}
|
||||
|
||||
this.jobs = jobs;
|
||||
} catch { /* non-critical */ }
|
||||
}
|
||||
|
||||
@@ -158,5 +178,11 @@ export class TranslateHistoryModel {
|
||||
total_records: this.metrics.reduce((s: number, m: Record<string, unknown>) => s + ((m.total_records as number) || 0), 0),
|
||||
};
|
||||
}
|
||||
|
||||
private _extractJobs(result: unknown): Record<string, unknown>[] {
|
||||
if (Array.isArray(result)) return result as Record<string, unknown>[];
|
||||
const payload = result as { items?: unknown[]; results?: unknown[] };
|
||||
return (payload?.items || payload?.results || []) as Record<string, unknown>[];
|
||||
}
|
||||
}
|
||||
// #endregion Translate.HistoryModel
|
||||
|
||||
@@ -86,10 +86,16 @@ describe("TranslateHistoryModel — Data Loading", () => {
|
||||
// #region TranslateHistoryModel.LoadRuns [C:2] [TYPE Test]
|
||||
it("loadRuns sets loading state and populates runs", async () => {
|
||||
vi.mocked(fetchAllRuns).mockResolvedValue({ items: [{ id: "r1" }, { id: "r2" }], total: 2 });
|
||||
model.filterDateFrom = "2026-07-01";
|
||||
model.filterDateTo = "2026-07-08";
|
||||
const promise = model.loadRuns();
|
||||
expect(model.isLoading).toBe(true);
|
||||
expect(model.uxState).toBe("loading");
|
||||
await promise;
|
||||
expect(fetchAllRuns).toHaveBeenCalledWith(expect.objectContaining({
|
||||
date_from: "2026-07-01",
|
||||
date_to: "2026-07-08",
|
||||
}));
|
||||
expect(model.runs).toHaveLength(2);
|
||||
expect(model.total).toBe(2);
|
||||
expect(model.uxState).toBe("populated");
|
||||
@@ -138,6 +144,16 @@ describe("TranslateHistoryModel — Data Loading", () => {
|
||||
expect(model.jobs).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("loadJobs fetches all pages when total exceeds first page", async () => {
|
||||
vi.mocked(fetchJobs)
|
||||
.mockResolvedValueOnce({ items: Array.from({ length: 100 }, (_, i) => ({ id: `j${i}` })), total: 101 })
|
||||
.mockResolvedValueOnce({ items: [{ id: "j100", name: "Job 100" }], total: 101 });
|
||||
await model.loadJobs();
|
||||
expect(fetchJobs).toHaveBeenCalledWith({ page: 1, page_size: 100 });
|
||||
expect(fetchJobs).toHaveBeenCalledWith({ page: 2, page_size: 100 });
|
||||
expect(model.jobs).toHaveLength(101);
|
||||
});
|
||||
|
||||
it("loadJobs accepts results format result", async () => {
|
||||
vi.mocked(fetchJobs).mockResolvedValue({ results: [{ id: "j1", name: "Job 1" }] });
|
||||
await model.loadJobs();
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="bg-surface-card border border-border rounded-lg p-4 mb-4">
|
||||
<div class="grid grid-cols-2 sm:grid-cols-5 gap-3">
|
||||
<div class="grid grid-cols-2 sm:grid-cols-6 gap-3">
|
||||
<select bind:value={m.filterJobId} class="px-3 py-2 border border-border-strong rounded-lg text-sm">
|
||||
<option value="">{$t.translate?.history?.all_jobs}</option>
|
||||
{#each m.jobs as job}<option value={job.id}>{job.name}</option>{/each}
|
||||
@@ -73,6 +73,7 @@
|
||||
<option value="baseline_expired">{$t.translate?.history?.trigger_baseline_expired}</option>
|
||||
</select>
|
||||
<input type="date" bind:value={m.filterDateFrom} class="px-3 py-2 border border-border-strong rounded-lg text-sm" placeholder={$t.translate?.common?.from} />
|
||||
<input type="date" bind:value={m.filterDateTo} class="px-3 py-2 border border-border-strong rounded-lg text-sm" placeholder={$t.translate?.common?.to} />
|
||||
<button onclick={() => { page0 = 0; m.applyFilters(); }} class="px-4 py-2 text-sm bg-primary text-white rounded-lg hover:bg-primary-hover">{$t.translate?.history?.filter}</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,6 +91,11 @@
|
||||
|
||||
<!-- Populated / Detail -->
|
||||
{:else if m.uxState === 'populated' || m.uxState === 'detail_open'}
|
||||
<div class="mb-3 text-sm text-text-muted">
|
||||
{_('translate.history.showing')
|
||||
.replace('{count}', String(Math.min(m.currentPage * m.pageSize, m.total)))
|
||||
.replace('{total}', String(m.total))}
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
{#each m.runs as run}
|
||||
<div onclick={() => m.openDetail(run)} class="bg-surface-card border border-border rounded-lg p-3 hover:shadow-sm hover:border-border-strong transition-all cursor-pointer">
|
||||
|
||||
Reference in New Issue
Block a user