diff --git a/ordinis-admin-ui/src/api/client.ts b/ordinis-admin-ui/src/api/client.ts index 2d2a55f..299eed0 100644 --- a/ordinis-admin-ui/src/api/client.ts +++ b/ordinis-admin-ui/src/api/client.ts @@ -794,6 +794,15 @@ export type SchemaTemplateDetail = SchemaTemplateSummary & { schemaJson: unknown } +/** + * AI Schema Assist suggest-field response. Shape должен matchить + * AiSchemaController output (parsed LLM JSON: {fieldName, schema}). + */ +export type AiFieldSuggestion = { + fieldName: string + schema: Record +} + /** * Empty-state hint payload (read-api scheduled-summary). Подсчёт записей с * {@code validFrom > now AND validTo > now} в текущем scope view. diff --git a/ordinis-admin-ui/src/api/mutations.ts b/ordinis-admin-ui/src/api/mutations.ts index d81d37e..111eb09 100644 --- a/ordinis-admin-ui/src/api/mutations.ts +++ b/ordinis-admin-ui/src/api/mutations.ts @@ -2,6 +2,7 @@ import { useCallback, useRef } from 'react' import { useMutation, useQueryClient } from '@tanstack/react-query' import { apiClient, + type AiFieldSuggestion, type BulkCloseRequest, type BulkCloseResponse, type CascadeCloseResult, @@ -741,3 +742,28 @@ export const useUpdateNotificationPreferences = () => { }, }) } + +/** + * AI Schema Assist — per-field suggest. Returns parsed {fieldName, schema}. + * Каллер показывает diff preview, user accept/edit/reject. + * + * 404 → AI disabled на бэкенде — frontend hides button (handled в caller). + * 503/circuit_open → temporary unavailable, show banner. + * 422 → bad output from LLM — show error message, suggest retry. + * 429 → rate limit, show "try again in a minute". + */ +export const useAiSuggestField = () => { + return useMutation({ + mutationFn: async (req: { + existingSchema: unknown + prompt: string + }): Promise => { + const { data } = await apiClient.post( + '/ai/suggest-field', + req, + { timeout: 30_000 }, // override default 10s — LLM может быть slow + ) + return data + }, + }) +} diff --git a/ordinis-admin-ui/src/api/queries.ts b/ordinis-admin-ui/src/api/queries.ts index 3c25b48..938a25b 100644 --- a/ordinis-admin-ui/src/api/queries.ts +++ b/ordinis-admin-ui/src/api/queries.ts @@ -852,6 +852,34 @@ export const schemaTemplateDetailQuery = (id: string) => }, staleTime: 5 * 60_000, }) + +// ───────────────────────────────────────────────────────────────────────────── +// AI feature detection — probe endpoint +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Probe AI Schema Assist availability. Backend регистрирует /ai/suggest-field + * conditionally на ordinis.ai.enabled — если выключено, OPTIONS возвращает 404. + * + *

Used UI чтобы decide показывать ли «AI suggest» button. Cache 5 min — + * feature flag меняется только при helm upgrade. + */ +export const aiFeatureAvailableQuery = queryOptions({ + queryKey: ['ai-feature-available'] as const, + queryFn: async (): Promise => { + try { + const { data } = await apiClient.get<{ enabled: boolean }>('/ai/info') + return Boolean(data?.enabled) + } catch { + // 404 (AI disabled) или network error → hide button (graceful default). + return false + } + }, + staleTime: 5 * 60_000, + retry: false, +}) + +export const useAiFeatureAvailable = () => useQuery(aiFeatureAvailableQuery) export const useRecordRaw = ( dictionaryName: string, businessKey: string | undefined, diff --git a/ordinis-admin-ui/src/components/schema/AiFieldSuggestPanel.tsx b/ordinis-admin-ui/src/components/schema/AiFieldSuggestPanel.tsx new file mode 100644 index 0000000..02ba594 --- /dev/null +++ b/ordinis-admin-ui/src/components/schema/AiFieldSuggestPanel.tsx @@ -0,0 +1,199 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import axios from 'axios' +import { SparkleIcon, XIcon } from '@phosphor-icons/react' +import { Alert, Button, TextArea } from '@/ui' +import { useAiSuggestField } from '@/api/mutations' +import { cn } from '@/lib/utils' + +/** + * AI Schema Assist — per-field suggest panel. + * + *

Expanded inline (не modal) — admin вводит описание → preview JSON suggestion + * → accept/reject. Accept callback получает {fieldName, schema} как plain JSON + * fragment, caller интегрирует в свой schema state. + * + *

Graceful degradation: + *

    + *
  • 404 от endpoint (AI disabled на бэке) → caller должен hide button entirely
  • + *
  • 503 circuit_open → banner «AI временно недоступен», retry в 5 мин
  • + *
  • 422 bad_output → error message, suggest другой prompt
  • + *
  • 429 rate_limit → «слишком много запросов, подожди минуту»
  • + *
+ */ +type Props = { + /** Current schema serialized as JSON object — будет передан как context */ + existingSchema: Record + /** Called когда юзер accepted suggestion */ + onAccept: (suggestion: { fieldName: string; schema: Record }) => void + /** Optional close handler — hide panel */ + onClose?: () => void +} + +export function AiFieldSuggestPanel({ existingSchema, onAccept, onClose }: Props) { + const { t } = useTranslation() + const [prompt, setPrompt] = useState('') + const [suggestion, setSuggestion] = useState<{ + fieldName: string + schema: Record + } | null>(null) + const mutation = useAiSuggestField() + + const handleSuggest = () => { + if (!prompt.trim()) return + setSuggestion(null) + mutation.mutate( + { existingSchema, prompt }, + { + onSuccess: (data) => setSuggestion(data), + }, + ) + } + + const handleAccept = () => { + if (!suggestion) return + onAccept(suggestion) + setPrompt('') + setSuggestion(null) + } + + const handleRetry = () => { + setSuggestion(null) + handleSuggest() + } + + const errorMessage = mutation.error ? formatError(mutation.error, t) : null + + return ( +
+
+ + + {t('aiSuggest.title', { defaultValue: 'AI: добавить поле' })} + + {onClose && ( + + )} +
+ +
+