[페이지 분류 강화 작업]
This commit is contained in:
@@ -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,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user