diff --git a/apps/api/app/infrastructure/repositories/ai_control.py b/apps/api/app/infrastructure/repositories/ai_control.py index fcd2d62..87a9666 100644 --- a/apps/api/app/infrastructure/repositories/ai_control.py +++ b/apps/api/app/infrastructure/repositories/ai_control.py @@ -281,6 +281,14 @@ class AiModelRepository: await self.session.flush() return _model_to_domain(row) + async def delete(self, model_id: str) -> bool: + row = await self.session.get(AiModelRow, model_id) + if row is None: + return False + await self.session.delete(row) + await self.session.flush() + return True + async def pick_for_mode(self, mode: RoutingMode) -> AiModel | None: """Choose a model for a routing mode. diff --git a/apps/api/app/interfaces/http/routers/admin.py b/apps/api/app/interfaces/http/routers/admin.py index ab1db48..73c2a3e 100644 --- a/apps/api/app/interfaces/http/routers/admin.py +++ b/apps/api/app/interfaces/http/routers/admin.py @@ -215,6 +215,28 @@ async def create_model( return _model_out(model) +@router.delete("/models/{model_id}", status_code=status.HTTP_200_OK) +async def delete_model( + model_id: str, + request: Request, + owner: User = Depends(require_platform_owner), + session: AsyncSession = Depends(get_session), +) -> dict: + repo = AiModelRepository(session) + existing = await repo.get(model_id) + if existing is None: + raise NotFoundError("مدلی با این شناسه یافت نشد.") + await repo.delete(model_id) + ip, ua = client_meta(request) + await record_audit( + session, action=AuditAction.MODEL_CHANGE, actor_user_id=owner.id, + target_type="model", target_id=model_id, + metadata={"internal_name": existing.internal_name, "deleted": True}, + ip=ip, user_agent=ua, + ) + return {"ok": True} + + # --------------------------------------------------------------------------- # Routing policies # --------------------------------------------------------------------------- diff --git a/apps/web/src/app/(app)/admin/models/page.tsx b/apps/web/src/app/(app)/admin/models/page.tsx index 0d47497..3f695ef 100644 --- a/apps/web/src/app/(app)/admin/models/page.tsx +++ b/apps/web/src/app/(app)/admin/models/page.tsx @@ -120,6 +120,9 @@ export default function AdminModelsPage() { * list updated without hunting for it (the list sits below the form). */ const [justAddedId, setJustAddedId] = React.useState(null); const listRef = React.useRef(null); + /** Inline delete-confirmation: which row is armed, and which is in flight. */ + const [confirmId, setConfirmId] = React.useState(null); + const [deletingId, setDeletingId] = React.useState(null); const load = React.useCallback(async () => { setLoading(true); @@ -272,6 +275,19 @@ export default function AdminModelsPage() { } }; + const onDelete = async (model: AdminModel) => { + setDeletingId(model.id); + try { + await api.adminDeleteModel(model.id); + setModels((current) => current.filter((m) => m.id !== model.id)); + setConfirmId(null); + } catch (err) { + setFormError(err instanceof ApiError ? err.message : "حذف مدل ناموفق بود."); + } finally { + setDeletingId(null); + } + }; + const selectedFamily = groupedModels.find((g) => g.base === modelId); const efforts: ReasoningEffort[] = selectedFamily ? (selectedFamily.efforts as ReasoningEffort[]) @@ -443,6 +459,7 @@ export default function AdminModelsPage() { ورودی خروجی وضعیت + عملیات @@ -487,6 +504,40 @@ export default function AdminModelsPage() { {model.is_active ? "فعال" : "غیرفعال"} + + {confirmId === model.id ? ( +
+ + +
+ ) : ( + + )} + ))} diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts index 2f39628..d4b9ac3 100644 --- a/apps/web/src/lib/api.ts +++ b/apps/web/src/lib/api.ts @@ -518,6 +518,10 @@ export const api = { method: "POST", body, }), + adminDeleteModel: (modelId: string) => + apiRequest(`/api/admin/models/${encodeURIComponent(modelId)}`, { + method: "DELETE", + }), adminRoutingPolicies: () => apiRequest("/api/admin/routing-policies"), adminCreateRoutingPolicy: (body: {