fix(ui): add navbar link to API Docs alongside system reset
Browse files- backend/src/models/jd.py +1 -0
- backend/src/routers/jds.py +6 -2
- backend/src/schemas/jd.py +2 -0
- frontend/src/app/layout.tsx +3 -0
- frontend/src/app/pipeline/page.tsx +10 -8
- frontend/src/app/sessions/[id]/page.tsx +4 -11
- frontend/src/lib/api.ts +3 -3
- nginx.conf +14 -0
backend/src/models/jd.py
CHANGED
|
@@ -25,5 +25,6 @@ class JobDescription(Base):
|
|
| 25 |
embedding_text: Mapped[str | None] = mapped_column(Text, nullable=True)
|
| 26 |
qdrant_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
| 27 |
status: Mapped[str] = mapped_column(String(32), default="pending")
|
|
|
|
| 28 |
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
| 29 |
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
|
|
|
| 25 |
embedding_text: Mapped[str | None] = mapped_column(Text, nullable=True)
|
| 26 |
qdrant_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
| 27 |
status: Mapped[str] = mapped_column(String(32), default="pending")
|
| 28 |
+
session_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), nullable=True)
|
| 29 |
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
| 30 |
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
backend/src/routers/jds.py
CHANGED
|
@@ -17,6 +17,7 @@ async def create_jd(payload: JDCreate, db: AsyncSession = Depends(get_db)):
|
|
| 17 |
id=uuid.uuid4(),
|
| 18 |
title=payload.title,
|
| 19 |
raw_text=payload.raw_text,
|
|
|
|
| 20 |
status="processing",
|
| 21 |
)
|
| 22 |
db.add(jd)
|
|
@@ -29,8 +30,11 @@ async def create_jd(payload: JDCreate, db: AsyncSession = Depends(get_db)):
|
|
| 29 |
|
| 30 |
|
| 31 |
@router.get("", response_model=list[JDListItem])
|
| 32 |
-
async def list_jds(db: AsyncSession = Depends(get_db)):
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
| 34 |
return result.scalars().all()
|
| 35 |
|
| 36 |
|
|
|
|
| 17 |
id=uuid.uuid4(),
|
| 18 |
title=payload.title,
|
| 19 |
raw_text=payload.raw_text,
|
| 20 |
+
session_id=payload.session_id,
|
| 21 |
status="processing",
|
| 22 |
)
|
| 23 |
db.add(jd)
|
|
|
|
| 30 |
|
| 31 |
|
| 32 |
@router.get("", response_model=list[JDListItem])
|
| 33 |
+
async def list_jds(session_id: uuid.UUID | None = None, db: AsyncSession = Depends(get_db)):
|
| 34 |
+
stmt = select(JobDescription).order_by(JobDescription.created_at.desc())
|
| 35 |
+
if session_id:
|
| 36 |
+
stmt = stmt.where(JobDescription.session_id == session_id)
|
| 37 |
+
result = await db.execute(stmt.limit(50))
|
| 38 |
return result.scalars().all()
|
| 39 |
|
| 40 |
|
backend/src/schemas/jd.py
CHANGED
|
@@ -7,6 +7,7 @@ from pydantic import BaseModel, Field
|
|
| 7 |
class JDCreate(BaseModel):
|
| 8 |
title: str
|
| 9 |
raw_text: str
|
|
|
|
| 10 |
|
| 11 |
|
| 12 |
class JDResponse(BaseModel):
|
|
@@ -30,6 +31,7 @@ class JDListItem(BaseModel):
|
|
| 30 |
id: UUID
|
| 31 |
title: str
|
| 32 |
status: str
|
|
|
|
| 33 |
jd_quality: dict[str, Any] = {}
|
| 34 |
created_at: datetime
|
| 35 |
|
|
|
|
| 7 |
class JDCreate(BaseModel):
|
| 8 |
title: str
|
| 9 |
raw_text: str
|
| 10 |
+
session_id: UUID | None = None
|
| 11 |
|
| 12 |
|
| 13 |
class JDResponse(BaseModel):
|
|
|
|
| 31 |
id: UUID
|
| 32 |
title: str
|
| 33 |
status: str
|
| 34 |
+
session_id: UUID | None = None
|
| 35 |
jd_quality: dict[str, Any] = {}
|
| 36 |
created_at: datetime
|
| 37 |
|
frontend/src/app/layout.tsx
CHANGED
|
@@ -20,6 +20,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
| 20 |
<Link href="/reset" className="px-3 py-1.5 rounded-lg text-xs font-medium text-slate-500 hover:text-red-400 transition-colors">
|
| 21 |
Reset
|
| 22 |
</Link>
|
|
|
|
|
|
|
|
|
|
| 23 |
<Link href="/pipeline" className="ml-2 px-3 py-1.5 rounded-lg text-sm font-semibold text-[var(--color-brand-light)] bg-[var(--color-brand-dim)] border border-[var(--color-brand-glow)] hover:bg-[var(--color-brand)] hover:text-white transition-all">
|
| 24 |
⚡ Auto Pipeline
|
| 25 |
</Link>
|
|
|
|
| 20 |
<Link href="/reset" className="px-3 py-1.5 rounded-lg text-xs font-medium text-slate-500 hover:text-red-400 transition-colors">
|
| 21 |
Reset
|
| 22 |
</Link>
|
| 23 |
+
<a href="/docs" target="_blank" rel="noreferrer" className="px-3 py-1.5 rounded-lg text-xs font-medium text-slate-500 hover:text-[var(--color-brand-light)] transition-colors">
|
| 24 |
+
API Docs ↗
|
| 25 |
+
</a>
|
| 26 |
<Link href="/pipeline" className="ml-2 px-3 py-1.5 rounded-lg text-sm font-semibold text-[var(--color-brand-light)] bg-[var(--color-brand-dim)] border border-[var(--color-brand-glow)] hover:bg-[var(--color-brand)] hover:text-white transition-all">
|
| 27 |
⚡ Auto Pipeline
|
| 28 |
</Link>
|
frontend/src/app/pipeline/page.tsx
CHANGED
|
@@ -127,17 +127,19 @@ export default function PipelinePage() {
|
|
| 127 |
updateState({ status: "uploading", sessionName, jdsInfo: jds, startTime: start, elapsedTime: 0 });
|
| 128 |
|
| 129 |
try {
|
| 130 |
-
// 1. Create Session
|
| 131 |
-
const
|
| 132 |
-
const
|
| 133 |
-
|
| 134 |
-
|
|
|
|
|
|
|
| 135 |
const jdIds = createdJDs.map(j => (j as any).id);
|
| 136 |
|
| 137 |
-
updateState({ sessionId:
|
| 138 |
|
| 139 |
-
//
|
| 140 |
-
const uploadRes = await api.uploadCandidates(file,
|
| 141 |
|
| 142 |
updateState({ status: "embedding", taskId: uploadRes.task_id });
|
| 143 |
|
|
|
|
| 127 |
updateState({ status: "uploading", sessionName, jdsInfo: jds, startTime: start, elapsedTime: 0 });
|
| 128 |
|
| 129 |
try {
|
| 130 |
+
// 1. Create Session first
|
| 131 |
+
const session = await api.createSession(sessionName, "Automated Candidate Batch Ingestion");
|
| 132 |
+
const sessionIdStr = (session as any).id;
|
| 133 |
+
|
| 134 |
+
// 2. Create JDs scoped to that session
|
| 135 |
+
const jdPromises = jds.map(jd => api.createJD(jd.title, jd.desc, sessionIdStr));
|
| 136 |
+
const createdJDs = await Promise.all(jdPromises);
|
| 137 |
const jdIds = createdJDs.map(j => (j as any).id);
|
| 138 |
|
| 139 |
+
updateState({ sessionId: sessionIdStr, jdIds });
|
| 140 |
|
| 141 |
+
// 3. Upload file
|
| 142 |
+
const uploadRes = await api.uploadCandidates(file, sessionIdStr);
|
| 143 |
|
| 144 |
updateState({ status: "embedding", taskId: uploadRes.task_id });
|
| 145 |
|
frontend/src/app/sessions/[id]/page.tsx
CHANGED
|
@@ -37,18 +37,11 @@ export default function SessionDetailPage({ params }: { params: Promise<{ id: st
|
|
| 37 |
|
| 38 |
const loadData = useCallback(async () => {
|
| 39 |
try {
|
| 40 |
-
const [s, jList] = await Promise.all([api.getSession(sessionId), api.listJDs()]);
|
| 41 |
setSession(s);
|
| 42 |
-
|
| 43 |
-
//
|
| 44 |
-
|
| 45 |
-
let filteredJDs = jList as JD[];
|
| 46 |
-
try {
|
| 47 |
-
const mapping = JSON.parse(localStorage.getItem("tp_session_jds") || "{}");
|
| 48 |
-
if (mapping[sessionId] && Array.isArray(mapping[sessionId]) && mapping[sessionId].length > 0) {
|
| 49 |
-
filteredJDs = filteredJDs.filter(jd => mapping[sessionId].includes(jd.id));
|
| 50 |
-
}
|
| 51 |
-
} catch (e) { }
|
| 52 |
setJDs(filteredJDs);
|
| 53 |
|
| 54 |
// If a JD is selected, load its match info automatically
|
|
|
|
| 37 |
|
| 38 |
const loadData = useCallback(async () => {
|
| 39 |
try {
|
| 40 |
+
const [s, jList] = await Promise.all([api.getSession(sessionId), api.listJDs(sessionId)]);
|
| 41 |
setSession(s);
|
| 42 |
+
|
| 43 |
+
// JDs are now natively scoped to the session id via the backend API
|
| 44 |
+
const filteredJDs = jList as JD[];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
setJDs(filteredJDs);
|
| 46 |
|
| 47 |
// If a JD is selected, load its match info automatically
|
frontend/src/lib/api.ts
CHANGED
|
@@ -120,9 +120,9 @@ export const api = {
|
|
| 120 |
getSession: (id: string) => request<SessionInfo>(`/api/sessions/${id}`),
|
| 121 |
deleteSession: (id: string) => request<void>(`/api/sessions/${id}`, { method: "DELETE" }),
|
| 122 |
|
| 123 |
-
createJD: (title: string, raw_text: string) =>
|
| 124 |
-
request<JD>("/api/jds", { method: "POST", body: JSON.stringify({ title, raw_text }) }),
|
| 125 |
-
listJDs: () => request<JD[]>("/api/jds"),
|
| 126 |
getJD: (id: string) => request<JD>(`/api/jds/${id}`),
|
| 127 |
updateJDWeights: (id: string, weights: Record<string, number>) =>
|
| 128 |
request<JD>(`/api/jds/${id}/weights`, { method: "PATCH", body: JSON.stringify({ weights }) }),
|
|
|
|
| 120 |
getSession: (id: string) => request<SessionInfo>(`/api/sessions/${id}`),
|
| 121 |
deleteSession: (id: string) => request<void>(`/api/sessions/${id}`, { method: "DELETE" }),
|
| 122 |
|
| 123 |
+
createJD: (title: string, raw_text: string, session_id?: string) =>
|
| 124 |
+
request<JD>("/api/jds", { method: "POST", body: JSON.stringify({ title, raw_text, session_id }) }),
|
| 125 |
+
listJDs: (session_id?: string) => request<JD[]>(session_id ? `/api/jds?session_id=${session_id}` : "/api/jds"),
|
| 126 |
getJD: (id: string) => request<JD>(`/api/jds/${id}`),
|
| 127 |
updateJDWeights: (id: string, weights: Record<string, number>) =>
|
| 128 |
request<JD>(`/api/jds/${id}/weights`, { method: "PATCH", body: JSON.stringify({ weights }) }),
|
nginx.conf
CHANGED
|
@@ -31,6 +31,20 @@ http {
|
|
| 31 |
proxy_pass http://127.0.0.1:8000/health;
|
| 32 |
}
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
location / {
|
| 35 |
proxy_pass http://127.0.0.1:3000;
|
| 36 |
proxy_set_header Host $host;
|
|
|
|
| 31 |
proxy_pass http://127.0.0.1:8000/health;
|
| 32 |
}
|
| 33 |
|
| 34 |
+
# Route FastAPI Swagger Docs correctly
|
| 35 |
+
location /docs {
|
| 36 |
+
proxy_pass http://127.0.0.1:8000/docs;
|
| 37 |
+
proxy_set_header Host $host;
|
| 38 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
# Route OpenAPI schema for Swagger
|
| 42 |
+
location /openapi.json {
|
| 43 |
+
proxy_pass http://127.0.0.1:8000/openapi.json;
|
| 44 |
+
proxy_set_header Host $host;
|
| 45 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
location / {
|
| 49 |
proxy_pass http://127.0.0.1:3000;
|
| 50 |
proxy_set_header Host $host;
|