[페이지 분류 강화 작업]

This commit is contained in:
LASTA_DEV01\lasta
2026-05-22 20:16:28 +09:00
parent 93980da14d
commit a358f221ff
29 changed files with 5633 additions and 52 deletions

View File

@@ -86,6 +86,26 @@ export function isCrawlTerminal(status: string): boolean {
return TERMINAL_CRAWL_STATUSES.has(status);
}
export const extractorModelsResponseSchema = z.object({
ok: z.boolean(),
error: z.string().optional(),
models: z
.array(
z
.object({
id: z.string(),
owned_by: z.string().optional(),
})
.passthrough(),
)
.optional()
.default([]),
});
export type ExtractorModelsResponse = z.infer<
typeof extractorModelsResponseSchema
>;
export const crawlApi = {
startByProject: (body: StartSiteCrawlRequest) =>
apiClient.post("/crawl-site/by-project", crawlJobSchema, body),
@@ -99,4 +119,9 @@ export const crawlApi = {
`/crawl-site/jobs/${encodeURIComponent(jobId)}/cancel`,
crawlJobSchema,
),
listExtractorModels: (provider: string, baseUrl?: string | null) =>
apiClient.post("/extractors/models", extractorModelsResponseSchema, {
provider,
base_url: baseUrl || null,
}),
};

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useForm } from "react-hook-form";
@@ -36,7 +36,7 @@ import {
useCrawlJob,
useStartSiteCrawl,
} from "@/hooks/useCrawl";
import { isCrawlTerminal } from "@/lib/api/crawl";
import { crawlApi, isCrawlTerminal } from "@/lib/api/crawl";
const startCrawlSchema = z.object({
source_name: z.string().min(1, "소스를 선택하세요"),
@@ -150,7 +150,53 @@ export default function CrawlPage() {
const sources = project?.sources ?? [];
const sourceName = watch("source_name");
const extractionMode = watch("extraction_mode");
const provider = watch("extractor_provider");
const baseUrl = watch("extractor_base_url");
const usesLlm = extractionMode !== "rule_only";
const [loadedModelHint, setLoadedModelHint] = useState<string>("");
const [modelLookupStatus, setModelLookupStatus] = useState<
"idle" | "loading" | "ok" | "error"
>("idle");
const [modelLookupError, setModelLookupError] = useState<string>("");
useEffect(() => {
if (!usesLlm || provider !== "lm_studio") {
setLoadedModelHint("");
setModelLookupStatus("idle");
setModelLookupError("");
return;
}
let cancelled = false;
setModelLookupStatus("loading");
setModelLookupError("");
(async () => {
try {
const res = await crawlApi.listExtractorModels(provider, baseUrl);
if (cancelled) return;
if (!res.ok) {
setModelLookupStatus("error");
setModelLookupError(res.error || "모델 조회 실패");
return;
}
const first = res.models?.[0]?.id;
if (!first) {
setModelLookupStatus("error");
setModelLookupError("로드된 모델이 없습니다");
return;
}
setLoadedModelHint(first);
setValue("extractor_model", first, { shouldDirty: false });
setModelLookupStatus("ok");
} catch (e) {
if (cancelled) return;
setModelLookupStatus("error");
setModelLookupError((e as Error).message);
}
})();
return () => {
cancelled = true;
};
}, [provider, baseUrl, usesLlm, setValue]);
const selectedSource = sources.find((s) => s.name === sourceName);
const progress = job?.progress;
const visited = progress?.visited_count ?? 0;
@@ -385,9 +431,30 @@ export default function CrawlPage() {
<Label htmlFor="extractor_model">Model</Label>
<Input
id="extractor_model"
placeholder="deepseek-r1-distill-qwen-7b"
placeholder={
loadedModelHint || "deepseek-r1-distill-qwen-7b"
}
{...register("extractor_model")}
/>
{provider === "lm_studio" &&
modelLookupStatus === "loading" && (
<p className="text-xs text-muted-foreground">
LM Studio ...
</p>
)}
{provider === "lm_studio" &&
modelLookupStatus === "ok" &&
loadedModelHint && (
<p className="text-xs text-muted-foreground">
LM Studio : {loadedModelHint}
</p>
)}
{provider === "lm_studio" &&
modelLookupStatus === "error" && (
<p className="text-xs text-destructive">
LM Studio : {modelLookupError}
</p>
)}
</div>
<div className="space-y-1.5">
<Label htmlFor="extractor_base_url">Base URL</Label>