refactor(modules): update classes, course-plans, diagnostic, questions, settings, student, layout
- Update classes data-access (invitations, main) for invitation management - Update course-plans actions, data-access, and types - Update diagnostic data-access for report queries - Update questions data-access for question bank queries - Update settings actions, ai-provider-settings-card, data-access, and types - Update student course-filters, student-courses-view, student-schedule-filters, student-schedule-view - Update layout app-sidebar, site-header, and navigation config
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, useTransition } from "react"
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, useTransition, type ReactElement } from "react"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { z } from "zod"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
@@ -39,9 +40,11 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/components/ui/select"
|
||||
import { Badge } from "@/shared/components/ui/badge"
|
||||
import { deleteAiProviderAction, getAiProviderSummaries, testAiProviderAction, upsertAiProviderAction, type AiProviderSummary } from "@/modules/settings/actions"
|
||||
|
||||
const ProviderSchema = z.enum(["zhipu", "openai", "gemini", "custom"])
|
||||
const VisibilitySchema = z.enum(["public", "private"])
|
||||
|
||||
const AiProviderFormSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
@@ -50,19 +53,26 @@ const AiProviderFormSchema = z.object({
|
||||
model: z.string().min(1, "Model is required"),
|
||||
apiKey: z.string().optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
visibility: VisibilitySchema.optional(),
|
||||
})
|
||||
|
||||
type AiProviderFormValues = z.infer<typeof AiProviderFormSchema>
|
||||
|
||||
const NEW_PROVIDER_VALUE = "__new__"
|
||||
|
||||
type AiProviderSettingsCardProps = {
|
||||
onProvidersChanged?: (rows: AiProviderSummary[]) => void
|
||||
initialMode?: "new" | "first"
|
||||
isAdmin?: boolean
|
||||
currentUserId?: string
|
||||
}
|
||||
|
||||
export function AiProviderSettingsCard({
|
||||
onProvidersChanged,
|
||||
initialMode = "first",
|
||||
}: {
|
||||
onProvidersChanged?: (rows: AiProviderSummary[]) => void
|
||||
initialMode?: "new" | "first"
|
||||
}) {
|
||||
isAdmin = false,
|
||||
currentUserId,
|
||||
}: AiProviderSettingsCardProps) {
|
||||
const t = useTranslations("settings.ai.providers")
|
||||
const [isPending, startTransition] = useTransition()
|
||||
const [providers, setProviders] = useState<AiProviderSummary[]>([])
|
||||
@@ -80,6 +90,7 @@ export function AiProviderSettingsCard({
|
||||
model: "",
|
||||
apiKey: "",
|
||||
isDefault: false,
|
||||
visibility: "private",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -108,6 +119,7 @@ export function AiProviderSettingsCard({
|
||||
model: "",
|
||||
apiKey: "",
|
||||
isDefault: false,
|
||||
visibility: "private",
|
||||
})
|
||||
}, [form])
|
||||
|
||||
@@ -138,6 +150,7 @@ export function AiProviderSettingsCard({
|
||||
model: next.model,
|
||||
apiKey: "",
|
||||
isDefault: next.isDefault,
|
||||
visibility: next.visibility,
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
@@ -163,6 +176,7 @@ export function AiProviderSettingsCard({
|
||||
model: next.model,
|
||||
apiKey: "",
|
||||
isDefault: next.isDefault,
|
||||
visibility: next.visibility,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -193,6 +207,7 @@ export function AiProviderSettingsCard({
|
||||
model: values.model.trim(),
|
||||
apiKey: apiKey || undefined,
|
||||
isDefault: values.isDefault ?? false,
|
||||
visibility: values.visibility,
|
||||
}
|
||||
const result = await testAiProviderAction(payload)
|
||||
if (result.success) {
|
||||
@@ -220,6 +235,7 @@ export function AiProviderSettingsCard({
|
||||
model: values.model.trim(),
|
||||
apiKey: values.apiKey?.trim() || undefined,
|
||||
isDefault: values.isDefault ?? false,
|
||||
visibility: values.visibility,
|
||||
}
|
||||
const result = await upsertAiProviderAction(payload)
|
||||
if (result.success) {
|
||||
@@ -245,6 +261,7 @@ export function AiProviderSettingsCard({
|
||||
model: next.model,
|
||||
apiKey: "",
|
||||
isDefault: next.isDefault,
|
||||
visibility: next.visibility,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
@@ -278,6 +295,7 @@ export function AiProviderSettingsCard({
|
||||
model: next.model,
|
||||
apiKey: "",
|
||||
isDefault: next.isDefault,
|
||||
visibility: next.visibility,
|
||||
})
|
||||
} else {
|
||||
resetToNew()
|
||||
@@ -291,6 +309,34 @@ export function AiProviderSettingsCard({
|
||||
})
|
||||
}
|
||||
|
||||
const renderProviderLabel = (item: AiProviderSummary): string => {
|
||||
const parts = [item.provider, "·", item.model]
|
||||
if (item.isDefault) parts.push(`(${t("setDefault")})`)
|
||||
return parts.join(" ")
|
||||
}
|
||||
|
||||
const renderVisibilityBadge = (item: AiProviderSummary): ReactElement => {
|
||||
const isOwner = currentUserId !== undefined && item.createdBy === currentUserId
|
||||
return (
|
||||
<span className="ml-2 inline-flex gap-1">
|
||||
{item.visibility === "public" ? (
|
||||
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 h-4">
|
||||
{t("badgePublic")}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-[10px] px-1.5 py-0 h-4">
|
||||
{t("badgePrivate")}
|
||||
</Badge>
|
||||
)}
|
||||
{isOwner ? (
|
||||
<Badge variant="outline" className="text-[10px] px-1.5 py-0 h-4 text-muted-foreground">
|
||||
{t("badgeOwner")}
|
||||
</Badge>
|
||||
) : null}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -312,7 +358,7 @@ export function AiProviderSettingsCard({
|
||||
<SelectItem value={NEW_PROVIDER_VALUE}>{t("createNew")}</SelectItem>
|
||||
{providers.map((item) => (
|
||||
<SelectItem key={item.id} value={item.id}>
|
||||
{item.provider} · {item.model}
|
||||
{renderProviderLabel(item)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -320,10 +366,13 @@ export function AiProviderSettingsCard({
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t("keyStatus")}</Label>
|
||||
<div className="rounded-md border px-3 py-2 text-sm text-muted-foreground">
|
||||
{selectedProvider?.apiKeyLast4
|
||||
? `${t("stored")} • ****${selectedProvider.apiKeyLast4}`
|
||||
: t("noKey")}
|
||||
<div className="flex items-center rounded-md border px-3 py-2 text-sm text-muted-foreground">
|
||||
{selectedProvider ? renderVisibilityBadge(selectedProvider) : null}
|
||||
<span className="ml-auto">
|
||||
{selectedProvider?.apiKeyLast4
|
||||
? `${t("stored")} • ****${selectedProvider.apiKeyLast4}`
|
||||
: t("noKey")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -374,6 +423,36 @@ export function AiProviderSettingsCard({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="visibility"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("visibility")}</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={field.value ?? "private"}
|
||||
onValueChange={field.onChange}
|
||||
disabled={!isAdmin}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="private">{t("visibilityPrivateLabel")}</SelectItem>
|
||||
{isAdmin ? (
|
||||
<SelectItem value="public">{t("visibilityPublicLabel")}</SelectItem>
|
||||
) : null}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{isAdmin ? t("visibilityDesc") : t("visibilityReadOnly")}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isDefault"
|
||||
|
||||
Reference in New Issue
Block a user