| const API_BASE = "http://127.0.0.1:8000/api"; |
| const STORAGE_KEY = "thr3shrMockStateV1"; |
|
|
| const defaultSettings = { |
| root_repo: "", |
| categories_root: "", |
| confidence_threshold: 0.6, |
| default_migrate_mode: "copy", |
| scan_recursive: true, |
| experimental_media_enabled: false, |
| experimental_style_detector_enabled: false, |
| hybrid_ml_on_review: true, |
| tagging_domain: "drawn", |
| sfw_classify_mode: false, |
| selected_tags: [], |
| selected_tags_nsfw: [], |
| max_inference_workers: 2, |
| inference_batch_size: 8, |
| force_cpu_inference: false, |
| tagger_model: "wd_swinv2_v3", |
| wd_general_threshold: 0.35, |
| }; |
| const mockTags = [ |
| "1girl", |
| "solo", |
| "blush", |
| "black_hair", |
| "brown_hair", |
| "blue_eyes", |
| "short_hair", |
| "long_hair", |
| "twintails", |
| "smile", |
| "open_mouth", |
| "looking_at_viewer", |
| ]; |
|
|
| const mockState = loadMockState(); |
| let backendAvailable = null; |
| const REQUEST_TIMEOUT_MS = 30000; |
|
|
| function loadMockState() { |
| try { |
| const raw = localStorage.getItem(STORAGE_KEY); |
| if (!raw) { |
| return { settings: { ...defaultSettings }, nextRunId: 1, runs: {} }; |
| } |
| const parsed = JSON.parse(raw); |
| return { |
| settings: { ...defaultSettings, ...(parsed.settings || {}) }, |
| nextRunId: parsed.nextRunId || 1, |
| runs: parsed.runs || {}, |
| }; |
| } catch (_) { |
| return { settings: { ...defaultSettings }, nextRunId: 1, runs: {} }; |
| } |
| } |
|
|
| function persistMockState() { |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(mockState)); |
| } |
|
|
| async function jsonRequest(path, options = {}) { |
| const controller = new AbortController(); |
| const timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : REQUEST_TIMEOUT_MS; |
| const { timeoutMs: _ignored, ...fetchOptions } = options; |
| const timeoutId = setTimeout(() => controller.abort("timeout"), timeoutMs); |
| try { |
| const response = await fetch(`${API_BASE}${path}`, { |
| headers: { "Content-Type": "application/json", ...(fetchOptions.headers || {}) }, |
| ...fetchOptions, |
| signal: fetchOptions.signal || controller.signal, |
| }); |
| const text = await response.text(); |
| const maybeJson = text ? (() => { |
| try { |
| return JSON.parse(text); |
| } catch (_) { |
| return null; |
| } |
| })() : null; |
| if (!response.ok) { |
| const detail = |
| (maybeJson && (maybeJson.detail || maybeJson.message)) || text || "Request failed"; |
| throw new Error(`HTTP ${response.status} ${response.statusText}: ${detail}`); |
| } |
| return maybeJson ?? {}; |
| } catch (err) { |
| if (err?.name === "AbortError") { |
| throw new Error(`Request timeout after ${timeoutMs / 1000}s`); |
| } |
| throw err; |
| } finally { |
| clearTimeout(timeoutId); |
| } |
| } |
|
|
| async function request(path, options = {}) { |
| if (backendAvailable === false) { |
| return mockRequest(path, options); |
| } |
| try { |
| const data = await jsonRequest(path, options); |
| backendAvailable = true; |
| return data; |
| } catch (err) { |
| const message = String(err?.message || ""); |
| const networkError = |
| err?.name === "TypeError" || |
| message.includes("Failed to fetch") || |
| message.includes("NetworkError") || |
| message.includes("Load failed") || |
| message.includes("CORS request did not succeed") || |
| message.includes("timeout"); |
| if (backendAvailable === null && networkError) { |
| backendAvailable = false; |
| return mockRequest(path, options); |
| } |
| throw err; |
| } |
| } |
|
|
| function parseBody(options) { |
| if (!options.body) return {}; |
| return JSON.parse(options.body); |
| } |
|
|
| function mockCounts(items) { |
| const byStatus = {}; |
| for (const item of items) { |
| byStatus[item.status] = (byStatus[item.status] || 0) + 1; |
| } |
| return Object.entries(byStatus).map(([status, count]) => ({ status, count })); |
| } |
|
|
| function makeMockItems(runId, selectedFolders, threshold) { |
| const pool = selectedFolders.length ? selectedFolders : ["1girl", "solo", "blush"]; |
| return pool.slice(0, 8).map((tag, idx) => { |
| const score = Math.max(0.3, 0.9 - idx * 0.06); |
| const needsReview = score < threshold; |
| return { |
| id: runId * 1000 + idx + 1, |
| run_id: runId, |
| file_path: `/mock/images/sample_${idx + 1}.jpg`, |
| relative_path: `sample_${idx + 1}.jpg`, |
| primary_tag: tag, |
| primary_score: score, |
| secondary_suggestions: pool |
| .filter((x) => x !== tag) |
| .slice(0, 3) |
| .map((x, i) => ({ tag: x, score: Math.max(0.2, score - 0.1 - i * 0.05) })), |
| global_top_tags: [tag, ...pool.filter((x) => x !== tag)] |
| .slice(0, 5) |
| .map((x, i) => ({ tag: x, score: Math.max(0.15, score - i * 0.07) })), |
| suggested_destination: `${mockState.settings.categories_root || "/mock/categories"}/${tag}`, |
| final_tag: tag, |
| final_destination: `${mockState.settings.categories_root || "/mock/categories"}/${tag}`, |
| status: needsReview ? "proposed" : "approved", |
| needs_review: needsReview, |
| review_reason: needsReview ? `Below threshold (${score.toFixed(3)} < ${threshold.toFixed(3)})` : null, |
| migrated_to: null, |
| full_scores: Object.fromEntries( |
| [tag, ...pool.filter((x) => x !== tag)].map((name, i) => [ |
| name, |
| Math.max(0.1, score - i * 0.08), |
| ]) |
| ), |
| }; |
| }); |
| } |
|
|
| function computeMockStatus(run) { |
| const total = run.total_images || 0; |
| const processed = run.processed_images || 0; |
| const pct = total > 0 ? Math.min(100, (processed / total) * 100) : 0; |
| return { |
| run_id: run.id, |
| status: run.status, |
| total_images: total, |
| processed_images: processed, |
| failed_images: run.failed_images || 0, |
| progress_pct: pct, |
| started_at: run.started_at || null, |
| finished_at: run.finished_at || null, |
| last_error: run.last_error || null, |
| cancel_requested: Boolean(run.cancel_requested), |
| has_items: (run.items || []).length > 0, |
| inference_mode: "single", |
| batch_size: 1, |
| avg_infer_ms_per_image: run.status === "running" ? 120 : null, |
| eta_seconds_remaining: |
| run.status === "running" && total > processed |
| ? Math.max(1, (total - processed) * 0.12) |
| : null, |
| eta_finish_at: |
| run.status === "running" && total > processed |
| ? new Date(Date.now() + Math.max(1, (total - processed) * 120)).toISOString() |
| : null, |
| queue_seed: null, |
| tagger_model: run.tagger_model || mockState.settings.tagger_model || "wd_swinv2_v3", |
| }; |
| } |
|
|
| function mockRequest(path, options = {}) { |
| const method = (options.method || "GET").toUpperCase(); |
| const body = parseBody(options); |
|
|
| if (path === "/settings" && method === "GET") { |
| return Promise.resolve(mockState.settings); |
| } |
| if (path === "/providers" && method === "GET") { |
| return Promise.resolve({ |
| available_providers: ["CPUExecutionProvider"], |
| cuda_available: false, |
| cpu_available: true, |
| forced_cpu: false, |
| likely_device: "cpu", |
| cuda_usable: false, |
| tagger_model: mockState.settings.tagger_model || "wd_swinv2_v3", |
| loaded_models: [], |
| mock_mode: true, |
| }); |
| } |
| if (path === "/models/unload" && method === "POST") { |
| return Promise.resolve({ |
| unloaded: [], |
| message: "No tagger sessions were resident in GPU memory.", |
| }); |
| } |
| if (path === "/debug/sfw-sources" && method === "GET") { |
| return Promise.resolve({ |
| sources: [ |
| { |
| id: "safebooru", |
| label: "Safebooru", |
| sfw_policy: "rating:safe", |
| max_content_tags: null, |
| }, |
| { |
| id: "danbooru", |
| label: "Danbooru", |
| sfw_policy: "rating:g", |
| max_content_tags: 2, |
| }, |
| ], |
| }); |
| } |
| if (path === "/debug/sfw-eval" && method === "POST") { |
| const tags = Array.isArray(body.tags) ? body.tags.filter(Boolean) : []; |
| if (!tags.length) { |
| return Promise.reject(new Error("At least one tag is required")); |
| } |
| const source = body.source || "safebooru"; |
| if (source === "danbooru" && tags.length > 2) { |
| return Promise.reject(new Error("Danbooru allows at most 2 content tag(s)")); |
| } |
| const count = Math.max(5, Math.min(30, Number(body.count) || 10)); |
| const threshold = Number(mockState.settings.confidence_threshold || 0.6); |
| const items = Array.from({ length: Math.min(count, 6) }, (_, idx) => { |
| const pullScores = Object.fromEntries(tags.map((t) => [t, Math.max(0.2, 0.95 - idx * 0.08)])); |
| const primary = tags[0] || "1girl"; |
| const score = pullScores[primary]; |
| const needsReview = score < threshold; |
| return { |
| source, |
| post_id: String(9000 + idx), |
| rating: source === "danbooru" ? "g" : "safe", |
| file_name: `${9000 + idx}.jpg`, |
| known_tags: tags, |
| pull_tag_scores: pullScores, |
| global_top_tags: tags.map((t) => ({ tag: t, score: pullScores[t] })), |
| primary_tag: needsReview ? null : primary, |
| primary_score: needsReview ? null : score, |
| needs_review: needsReview, |
| review_reason: needsReview ? `Below threshold (${score.toFixed(3)} < ${threshold}).` : null, |
| suggested_folder: needsReview ? null : primary, |
| secondary_suggestions: needsReview ? [{ tag: primary, score }] : [], |
| }; |
| }); |
| return Promise.resolve({ |
| source, |
| source_label: source === "danbooru" ? "Danbooru" : "Safebooru", |
| sfw_policy: source === "danbooru" ? "rating:g" : "rating:safe", |
| query: `${tags.join(" ")} ${source === "danbooru" ? "rating:g" : "rating:safe"}`, |
| tags, |
| count_requested: count, |
| count_evaluated: items.length, |
| tagger_model: mockState.settings.tagger_model || "wd_swinv2_v3", |
| confidence_threshold: threshold, |
| destination_tags: mockState.settings.selected_tags || [], |
| recall: tags.map((tag) => ({ |
| tag, |
| present_in_posts: items.length, |
| hits_at_threshold: items.filter((it) => Number(it.pull_tag_scores[tag] || 0) >= threshold) |
| .length, |
| hit_rate: items.length |
| ? items.filter((it) => Number(it.pull_tag_scores[tag] || 0) >= threshold).length / |
| items.length |
| : null, |
| })), |
| items, |
| errors: [], |
| }); |
| } |
| if (path === "/scan/preview" && method === "GET") { |
| return Promise.resolve({ |
| root_repo: mockState.settings.root_repo || "/mock/images", |
| recursive: Boolean(mockState.settings.scan_recursive), |
| experimental_media_enabled: Boolean(mockState.settings.experimental_media_enabled), |
| excluded_dirs: mockState.settings.categories_root |
| ? [mockState.settings.categories_root] |
| : [], |
| stats: { |
| total_files: 12, |
| eligible_images: 8, |
| ignored_unsupported: 3, |
| ignored_gif: 1, |
| failed_to_read: 0, |
| }, |
| sample_paths: ["/mock/images/sample_1.jpg"], |
| }); |
| } |
| if (path.startsWith("/tags") && method === "GET") { |
| const queryString = path.includes("?") ? path.split("?")[1] : ""; |
| const params = new URLSearchParams(queryString); |
| const query = (params.get("query") || "").toLowerCase(); |
| const limit = Number(params.get("limit") || 50); |
| const items = mockTags.filter((t) => t.toLowerCase().includes(query)).slice(0, limit); |
| return Promise.resolve({ items, count: items.length }); |
| } |
| if (path === "/settings" && method === "PUT") { |
| const next = { ...defaultSettings, ...mockState.settings, ...body }; |
| if (next.tagging_domain === "real_life") { |
| next.sfw_classify_mode = false; |
| } |
| if (next.sfw_classify_mode) { |
| next.selected_tags = ["SFW", "scenery"]; |
| } |
| if (!Array.isArray(next.selected_tags_nsfw)) { |
| next.selected_tags_nsfw = []; |
| } |
| mockState.settings = next; |
| persistMockState(); |
| return Promise.resolve(mockState.settings); |
| } |
| if (path === "/runs/start" && method === "POST") { |
| mockState.settings = { ...mockState.settings, ...body }; |
| const runId = mockState.nextRunId++; |
| const selectedFolders = body.selected_folders || []; |
| const threshold = Number(mockState.settings.confidence_threshold || 0.6); |
| const items = makeMockItems(runId, selectedFolders, threshold); |
| const mappings = selectedFolders.map((name) => ({ |
| folder_name: name, |
| normalized_name: name.toLowerCase(), |
| matched_tag: name.toLowerCase(), |
| matched: true, |
| })); |
| mockState.runs[runId] = { |
| id: runId, |
| root_repo: mockState.settings.root_repo, |
| categories_root: mockState.settings.categories_root, |
| confidence_threshold: threshold, |
| tagger_model: mockState.settings.tagger_model || "wd_swinv2_v3", |
| status: "pending", |
| total_images: items.length, |
| processed_images: 0, |
| failed_images: 0, |
| cancel_requested: 0, |
| started_at: new Date().toISOString(), |
| finished_at: null, |
| last_error: null, |
| items, |
| }; |
| persistMockState(); |
| return Promise.resolve({ |
| run_id: runId, |
| status: "pending", |
| stats: null, |
| mappings, |
| unmatched_folders: [], |
| created_items: 0, |
| message: "Run queued; poll /api/runs/{run_id}/status for progress.", |
| }); |
| } |
| if (path.match(/^\/runs\/\d+\/status$/) && method === "GET") { |
| const runId = Number(path.split("/")[2]); |
| const run = mockState.runs[runId]; |
| if (!run) { |
| return Promise.reject(new Error("Run not found")); |
| } |
| if (run.status === "pending") { |
| run.status = "running"; |
| } |
| if (run.status === "running" && !run.cancel_requested) { |
| run.processed_images = Math.min(run.total_images, run.processed_images + 5); |
| if (run.processed_images >= run.total_images) { |
| run.status = "completed"; |
| run.finished_at = new Date().toISOString(); |
| } |
| persistMockState(); |
| } |
| if (run.cancel_requested && run.status === "running") { |
| run.status = "cancelled"; |
| run.finished_at = new Date().toISOString(); |
| persistMockState(); |
| } |
| return Promise.resolve(computeMockStatus(run)); |
| } |
| if (path.match(/^\/runs\/\d+\/cancel$/) && method === "POST") { |
| const runId = Number(path.split("/")[2]); |
| const run = mockState.runs[runId]; |
| if (!run) { |
| return Promise.reject(new Error("Run not found")); |
| } |
| run.cancel_requested = 1; |
| if (run.status === "pending") { |
| run.status = "cancelled"; |
| run.finished_at = new Date().toISOString(); |
| } |
| persistMockState(); |
| return Promise.resolve(computeMockStatus(run)); |
| } |
| if (path.match(/^\/runs\/\d+\/reclassify$/) && method === "POST") { |
| const runId = Number(path.split("/")[2]); |
| const run = mockState.runs[runId]; |
| if (!run) { |
| return Promise.reject(new Error("Run not found")); |
| } |
| if (["pending", "running"].includes(run.status)) { |
| return Promise.reject(new Error("Cannot reclassify while a run is still pending or running.")); |
| } |
| const threshold = Number(run.confidence_threshold || mockState.settings.confidence_threshold || 0.6); |
| const model = body.tagger_model || "wd_eva02_large"; |
| const idFilter = Array.isArray(body.item_ids) ? new Set(body.item_ids) : null; |
| let eligibleCount = 0; |
| run.items = run.items.map((item) => { |
| if (item.status !== "proposed" || !item.needs_review) return item; |
| if (idFilter && !idFilter.has(item.id)) return item; |
| eligibleCount += 1; |
| const boosted = Math.min(0.99, Number(item.primary_score || 0.4) + 0.35); |
| const tag = item.primary_tag || item.secondary_suggestions?.[0]?.tag || "1girl"; |
| if (boosted >= threshold) { |
| return { |
| ...item, |
| primary_tag: tag, |
| primary_score: boosted, |
| needs_review: false, |
| review_reason: null, |
| status: "approved", |
| final_tag: item.final_tag || tag, |
| suggested_destination: `${mockState.settings.categories_root || "/mock/categories"}/${tag}`, |
| }; |
| } |
| return { |
| ...item, |
| review_reason: `Reclassified with ${model}. Below threshold (${boosted.toFixed(3)} < ${threshold.toFixed(3)}).`, |
| }; |
| }); |
| if (eligibleCount === 0) { |
| return Promise.reject(new Error("No eligible needs-review items to reclassify")); |
| } |
| run.status = "running"; |
| run.cancel_requested = 0; |
| run.finished_at = null; |
| persistMockState(); |
| setTimeout(() => { |
| if (run.cancel_requested) { |
| run.status = "cancelled"; |
| } else { |
| run.status = "completed"; |
| } |
| run.finished_at = new Date().toISOString(); |
| persistMockState(); |
| }, 400); |
| return Promise.resolve({ |
| run_id: runId, |
| status: "running", |
| eligible_count: eligibleCount, |
| tagger_model: model, |
| message: `Reclassify queued for ${eligibleCount} item(s) with ${model}`, |
| }); |
| } |
| if (path.startsWith("/runs/") && path.endsWith("/items") && method === "GET") { |
| const runId = Number(path.split("/")[2]); |
| const run = mockState.runs[runId]; |
| if (!run) return Promise.resolve([]); |
| const visibleCount = Math.max(0, run.processed_images || 0); |
| return Promise.resolve(run.items.slice(0, visibleCount)); |
| } |
| if (path.startsWith("/runs/") && !path.includes("/items") && method === "GET") { |
| const runId = Number(path.split("/")[2]); |
| const run = mockState.runs[runId]; |
| if (!run) return Promise.resolve({ run: null, counts: [] }); |
| const visibleItems = run.items.slice(0, Math.max(0, run.processed_images || 0)); |
| return Promise.resolve({ run, counts: mockCounts(visibleItems) }); |
| } |
| if (path.startsWith("/items/") && method === "PATCH") { |
| const itemId = Number(path.split("/")[2]); |
| const run = Object.values(mockState.runs).find((r) => r.items.some((i) => i.id === itemId)); |
| if (!run) return Promise.resolve(null); |
| const item = run.items.find((i) => i.id === itemId); |
| if (body.status) item.status = body.status; |
| if (body.final_tag) { |
| item.final_tag = body.final_tag; |
| item.final_destination = `${mockState.settings.categories_root || "/mock/categories"}/${body.final_tag}`; |
| } |
| item.needs_review = false; |
| item.review_reason = null; |
| persistMockState(); |
| return Promise.resolve(item); |
| } |
| if (path.match(/^\/items\/\d+\/scores$/) && method === "GET") { |
| const itemId = Number(path.split("/")[2]); |
| const run = Object.values(mockState.runs).find((r) => r.items.some((i) => i.id === itemId)); |
| if (!run) return Promise.reject(new Error("Item not found")); |
| const item = run.items.find((i) => i.id === itemId); |
| return Promise.resolve({ item_id: item.id, full_scores: item.full_scores || {} }); |
| } |
| if (path.match(/^\/runs\/\d+\/batch$/) && method === "POST") { |
| const runId = Number(path.split("/")[2]); |
| const run = mockState.runs[runId]; |
| if (!run) return Promise.resolve({ updated: 0 }); |
| let updated = 0; |
| for (const item of run.items) { |
| if (!body.item_ids.includes(item.id)) continue; |
| if (body.status) item.status = body.status; |
| if (body.final_tag) { |
| item.final_tag = body.final_tag; |
| item.final_destination = `${mockState.settings.categories_root || "/mock/categories"}/${body.final_tag}`; |
| } |
| item.needs_review = false; |
| item.review_reason = null; |
| updated += 1; |
| } |
| persistMockState(); |
| return Promise.resolve({ updated }); |
| } |
| if (path.match(/^\/runs\/\d+\/migrate$/) && method === "POST") { |
| const runId = Number(path.split("/")[2]); |
| const run = mockState.runs[runId]; |
| if (!run) return Promise.resolve({ mode: body.mode, total_candidates: 0, migrated_count: 0, failed_count: 0, results: [] }); |
| const approved = run.items.filter((x) => x.status === "approved"); |
| const results = approved.map((item) => { |
| item.status = "migrated"; |
| item.migrated_to = `${item.final_destination}/${item.relative_path}`; |
| return { |
| item_id: item.id, |
| source: item.file_path, |
| destination: item.migrated_to, |
| success: true, |
| error: null, |
| }; |
| }); |
| persistMockState(); |
| return Promise.resolve({ |
| mode: body.mode, |
| total_candidates: approved.length, |
| migrated_count: approved.length, |
| failed_count: 0, |
| results, |
| }); |
| } |
|
|
| return Promise.reject(new Error(`Unsupported mock route: ${method} ${path}`)); |
| } |
|
|
| export const api = { |
| isOfflineMode: () => backendAvailable === false, |
| getTags: (query = "", limit = 50, domain = null) => { |
| const params = new URLSearchParams({ query, limit: String(limit) }); |
| if (domain) params.set("domain", domain); |
| return request(`/tags?${params.toString()}`); |
| }, |
| getSettings: () => request("/settings"), |
| getProviders: () => request("/providers"), |
| unloadModels: () => |
| request("/models/unload", { |
| method: "POST", |
| body: JSON.stringify({}), |
| }), |
| getRealLifeStatus: () => request("/real-life/status"), |
| previewScan: () => request("/scan/preview"), |
| saveSettings: (payload) => |
| request("/settings", { |
| method: "PUT", |
| body: JSON.stringify(payload), |
| }), |
| startRun: (payload) => |
| request("/runs/start", { |
| method: "POST", |
| body: JSON.stringify(payload), |
| }), |
| getRun: (runId) => request(`/runs/${runId}`), |
| getRunStatus: (runId) => request(`/runs/${runId}/status`), |
| cancelRun: (runId) => |
| request(`/runs/${runId}/cancel`, { |
| method: "POST", |
| body: JSON.stringify({}), |
| }), |
| reclassifyRun: (runId, payload) => |
| request(`/runs/${runId}/reclassify`, { |
| method: "POST", |
| body: JSON.stringify(payload), |
| }), |
| getSfwDebugSources: () => request("/debug/sfw-sources"), |
| runSfwDebugEval: (payload) => |
| request("/debug/sfw-eval", { |
| method: "POST", |
| body: JSON.stringify(payload), |
| timeoutMs: 180000, |
| }), |
| getSfwDebugPreviewUrl: (sourceId, fileName) => { |
| if (backendAvailable === false) return null; |
| return `${API_BASE}/debug/sfw-eval/preview/${encodeURIComponent(sourceId)}/${encodeURIComponent(fileName)}`; |
| }, |
| runRealismDebugEval: (payload) => |
| request("/debug/realism-eval", { |
| method: "POST", |
| body: JSON.stringify(payload), |
| |
| timeoutMs: 60 * 60 * 1000, |
| }), |
| getRealismDebugPreviewUrl: (sourceId, fileName) => { |
| if (backendAvailable === false) return null; |
| return `${API_BASE}/debug/realism-eval/preview/${encodeURIComponent(sourceId)}/${encodeURIComponent(fileName)}`; |
| }, |
| getStyleDetectors: () => request("/debug/style-detectors"), |
| runStyleDebugEval: (payload) => |
| request("/debug/style-eval", { |
| method: "POST", |
| body: JSON.stringify(payload), |
| timeoutMs: 60 * 60 * 1000, |
| }), |
| runTagRecallEval: (payload) => |
| request("/debug/tag-recall-eval", { |
| method: "POST", |
| body: JSON.stringify(payload), |
| |
| timeoutMs: 60 * 60 * 1000, |
| }), |
| getTagRecallPreviewUrl: (sourceId, fileName) => { |
| if (backendAvailable === false) return null; |
| return `${API_BASE}/debug/tag-recall-eval/preview/${encodeURIComponent(sourceId)}/${encodeURIComponent(fileName)}`; |
| }, |
| runTagFpEval: (payload) => |
| request("/debug/tag-fp-eval", { |
| method: "POST", |
| body: JSON.stringify(payload), |
| timeoutMs: 60 * 60 * 1000, |
| }), |
| getRunItems: (runId, filters = {}) => { |
| const params = new URLSearchParams(); |
| if (filters.status) params.set("status", filters.status); |
| if (filters.needs_review !== undefined) { |
| params.set("needs_review", String(filters.needs_review)); |
| } |
| if (filters.include_scores) { |
| params.set("include_scores", "true"); |
| } |
| const query = params.toString(); |
| return request(`/runs/${runId}/items${query ? `?${query}` : ""}`, { |
| timeoutMs: Number(filters.timeoutMs) > 0 ? Number(filters.timeoutMs) : 120000, |
| }); |
| }, |
| getItemScores: (itemId) => request(`/items/${itemId}/scores`), |
| updateItem: (itemId, payload) => |
| request(`/items/${itemId}`, { |
| method: "PATCH", |
| body: JSON.stringify(payload), |
| }), |
| batchUpdate: (runId, payload) => |
| request(`/runs/${runId}/batch`, { |
| method: "POST", |
| body: JSON.stringify(payload), |
| }), |
| migrateRun: (runId, payload) => |
| request(`/runs/${runId}/migrate`, { |
| method: "POST", |
| body: JSON.stringify(payload), |
| |
| timeoutMs: 60 * 60 * 1000, |
| }), |
| getItemPreviewUrl: (itemId) => { |
| if (backendAvailable === false) return null; |
| return `${API_BASE}/items/${itemId}/preview`; |
| }, |
| }; |
|
|