feat: implement AI service and prompt management pages for tenant configuration
This commit is contained in:
parent
b16e6d9c18
commit
85ecd9dac1
@ -9,9 +9,11 @@ import {
|
||||
SecondaryButton,
|
||||
FormSlider,
|
||||
FormTagInput,
|
||||
MultiselectPaginatedSelect,
|
||||
} from "@/components/shared";
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import { aiService } from "@/services/ai-service";
|
||||
import { moduleService } from "@/services/module-service";
|
||||
import { showToast } from "@/utils/toast";
|
||||
import { useForm, useFieldArray, Controller } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@ -39,6 +41,7 @@ const promptSchema = z.object({
|
||||
tags: z.array(z.string()),
|
||||
is_default: z.boolean(),
|
||||
variables: z.array(variableSchema),
|
||||
module_ids: z.array(z.string().uuid()).optional(),
|
||||
});
|
||||
|
||||
type PromptFormData = z.infer<typeof promptSchema>;
|
||||
@ -47,6 +50,7 @@ const PromptCreate = (): ReactElement => {
|
||||
const navigate = useNavigate();
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
const [apiProviders, setApiProviders] = useState<AIProviderInfo[]>([]);
|
||||
const [modules, setModules] = useState<Array<{ value: string; label: string }>>([]);
|
||||
|
||||
const {
|
||||
control,
|
||||
@ -76,6 +80,7 @@ const PromptCreate = (): ReactElement => {
|
||||
default: "",
|
||||
},
|
||||
],
|
||||
module_ids: [],
|
||||
},
|
||||
});
|
||||
|
||||
@ -100,19 +105,53 @@ const PromptCreate = (): ReactElement => {
|
||||
useEffect(() => {
|
||||
const loadMeta = async (): Promise<void> => {
|
||||
try {
|
||||
const providerData = await aiService.getProviders();
|
||||
const [providerData, modulesRes] = await Promise.all([
|
||||
aiService.getProviders(),
|
||||
moduleService.getMyModules(),
|
||||
]);
|
||||
setApiProviders(providerData);
|
||||
setModules(
|
||||
(modulesRes.data || []).map((m: any) => ({
|
||||
value: m.id,
|
||||
label: m.name,
|
||||
}))
|
||||
);
|
||||
} catch (err: unknown) {
|
||||
showToast.error(
|
||||
(err as { response?: { data?: { error?: { message?: string } } } })
|
||||
?.response?.data?.error?.message ||
|
||||
"Failed to load provider metadata",
|
||||
"Failed to load provider or module metadata",
|
||||
);
|
||||
}
|
||||
};
|
||||
void loadMeta();
|
||||
}, []);
|
||||
|
||||
const handleLoadModules = async (
|
||||
page: number,
|
||||
limit: number,
|
||||
search?: string
|
||||
) => {
|
||||
let filtered = modules;
|
||||
if (search) {
|
||||
filtered = modules.filter((m) =>
|
||||
m.label.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
}
|
||||
const startIndex = (page - 1) * limit;
|
||||
const paginatedOptions = filtered.slice(startIndex, startIndex + limit);
|
||||
return {
|
||||
options: paginatedOptions,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total: filtered.length,
|
||||
totalPages: Math.ceil(filtered.length / limit),
|
||||
hasMore: startIndex + limit < filtered.length,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const onFormSubmit = async (data: PromptFormData): Promise<void> => {
|
||||
const parsedTags = data.tags || [];
|
||||
|
||||
@ -140,6 +179,7 @@ const PromptCreate = (): ReactElement => {
|
||||
variables: sanitizedVariables,
|
||||
tags: parsedTags,
|
||||
is_default: data.is_default,
|
||||
module_ids: data.module_ids,
|
||||
});
|
||||
showToast.success("Prompt created successfully");
|
||||
navigate("/tenant/ai/prompts");
|
||||
@ -559,6 +599,23 @@ const PromptCreate = (): ReactElement => {
|
||||
<h2 className="text-sm font-semibold text-gray-800">
|
||||
Organization
|
||||
</h2>
|
||||
<Controller
|
||||
name="module_ids"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<MultiselectPaginatedSelect
|
||||
label="Associated Modules"
|
||||
placeholder="Select Modules"
|
||||
value={field.value || []}
|
||||
onValueChange={field.onChange}
|
||||
onLoadOptions={handleLoadModules}
|
||||
initialOptions={modules}
|
||||
isSearchable
|
||||
multiple
|
||||
error={errors.module_ids?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="tags"
|
||||
control={control}
|
||||
|
||||
@ -8,10 +8,12 @@ import {
|
||||
PrimaryButton,
|
||||
SecondaryButton,
|
||||
FormSlider,
|
||||
FormTagInput
|
||||
FormTagInput,
|
||||
MultiselectPaginatedSelect
|
||||
} from "@/components/shared";
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import { aiService } from "@/services/ai-service";
|
||||
import { moduleService } from "@/services/module-service";
|
||||
import { showToast } from "@/utils/toast";
|
||||
import { useForm, useFieldArray, Controller } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@ -40,6 +42,7 @@ const promptSchema = z.object({
|
||||
is_default: z.boolean(),
|
||||
variables: z.array(variableSchema),
|
||||
change_notes: z.string().optional(),
|
||||
module_ids: z.array(z.string().uuid()).optional(),
|
||||
});
|
||||
|
||||
type PromptFormData = z.infer<typeof promptSchema>;
|
||||
@ -50,6 +53,7 @@ const PromptEdit = (): ReactElement => {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
const [apiProviders, setApiProviders] = useState<AIProviderInfo[]>([]);
|
||||
const [modules, setModules] = useState<Array<{ value: string; label: string }>>([]);
|
||||
const [currentVersion, setCurrentVersion] = useState<number>(1);
|
||||
|
||||
const {
|
||||
@ -75,6 +79,7 @@ const PromptEdit = (): ReactElement => {
|
||||
is_default: false,
|
||||
variables: [],
|
||||
change_notes: "",
|
||||
module_ids: [],
|
||||
},
|
||||
});
|
||||
|
||||
@ -101,12 +106,19 @@ const PromptEdit = (): ReactElement => {
|
||||
if (!id) return;
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const [providerData, promptData] = await Promise.all([
|
||||
const [providerData, promptData, modulesRes] = await Promise.all([
|
||||
aiService.getProviders(),
|
||||
aiService.getPrompt(id),
|
||||
moduleService.getMyModules(),
|
||||
]);
|
||||
|
||||
setApiProviders(providerData);
|
||||
setModules(
|
||||
(modulesRes.data || []).map((m: any) => ({
|
||||
value: m.id,
|
||||
label: m.name,
|
||||
}))
|
||||
);
|
||||
|
||||
reset({
|
||||
name: promptData.name,
|
||||
@ -127,6 +139,7 @@ const PromptEdit = (): ReactElement => {
|
||||
default: v.default ? String(v.default) : "",
|
||||
})),
|
||||
change_notes: "",
|
||||
module_ids: promptData.moduleIds || [],
|
||||
});
|
||||
setCurrentVersion(promptData.version || 1);
|
||||
} catch (err: unknown) {
|
||||
@ -143,6 +156,31 @@ const PromptEdit = (): ReactElement => {
|
||||
void loadData();
|
||||
}, [id, reset, navigate]);
|
||||
|
||||
const handleLoadModules = async (
|
||||
page: number,
|
||||
limit: number,
|
||||
search?: string
|
||||
) => {
|
||||
let filtered = modules;
|
||||
if (search) {
|
||||
filtered = modules.filter((m) =>
|
||||
m.label.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
}
|
||||
const startIndex = (page - 1) * limit;
|
||||
const paginatedOptions = filtered.slice(startIndex, startIndex + limit);
|
||||
return {
|
||||
options: paginatedOptions,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total: filtered.length,
|
||||
totalPages: Math.ceil(filtered.length / limit),
|
||||
hasMore: startIndex + limit < filtered.length,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const onFormSubmit = async (data: PromptFormData): Promise<void> => {
|
||||
if (!id) return;
|
||||
const parsedTags = data.tags || [];
|
||||
@ -172,6 +210,7 @@ const PromptEdit = (): ReactElement => {
|
||||
tags: parsedTags,
|
||||
is_default: data.is_default,
|
||||
change_notes: data.change_notes,
|
||||
module_ids: data.module_ids,
|
||||
});
|
||||
showToast.success("Prompt updated successfully");
|
||||
navigate("/tenant/ai/prompts");
|
||||
@ -601,6 +640,23 @@ const PromptEdit = (): ReactElement => {
|
||||
<h2 className="text-sm font-semibold text-gray-800">
|
||||
Organization
|
||||
</h2>
|
||||
<Controller
|
||||
name="module_ids"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<MultiselectPaginatedSelect
|
||||
label="Associated Modules"
|
||||
placeholder="Select Modules"
|
||||
value={field.value || []}
|
||||
onValueChange={field.onChange}
|
||||
onLoadOptions={handleLoadModules}
|
||||
initialOptions={modules}
|
||||
isSearchable
|
||||
multiple
|
||||
error={errors.module_ids?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="tags"
|
||||
control={control}
|
||||
|
||||
@ -19,8 +19,10 @@ import {
|
||||
ActionDropdown,
|
||||
DeleteConfirmationModal,
|
||||
SearchBox,
|
||||
FilterDropdown,
|
||||
} from "@/components/shared";
|
||||
import { aiService } from "@/services/ai-service";
|
||||
import { moduleService } from "@/services/module-service";
|
||||
import type { AIPrompt } from "@/types/ai";
|
||||
import { showToast } from "@/utils/toast";
|
||||
import { formatDate } from "@/utils/format-date";
|
||||
@ -43,6 +45,10 @@ const PromptManagement = (): ReactElement => {
|
||||
totalPages: 1,
|
||||
});
|
||||
|
||||
// Module filter states
|
||||
const [modules, setModules] = useState<Array<{ value: string; label: string }>>([]);
|
||||
const [selectedModuleId, setSelectedModuleId] = useState<string>("");
|
||||
|
||||
// Modal states
|
||||
const [selectedPrompt, setSelectedPrompt] = useState<AIPrompt | null>(null);
|
||||
const [isVersionsOpen, setIsVersionsOpen] = useState(false);
|
||||
@ -57,6 +63,24 @@ const PromptManagement = (): ReactElement => {
|
||||
return () => clearTimeout(timer);
|
||||
}, [search]);
|
||||
|
||||
// Load modules list on mount
|
||||
useEffect(() => {
|
||||
const loadModules = async (): Promise<void> => {
|
||||
try {
|
||||
const res = await moduleService.getMyModules();
|
||||
setModules(
|
||||
(res.data || []).map((m: any) => ({
|
||||
value: m.id,
|
||||
label: m.name,
|
||||
}))
|
||||
);
|
||||
} catch (err: any) {
|
||||
console.error("Failed to load modules list", err);
|
||||
}
|
||||
};
|
||||
void loadModules();
|
||||
}, []);
|
||||
|
||||
const loadPrompts = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
@ -65,6 +89,7 @@ const PromptManagement = (): ReactElement => {
|
||||
page,
|
||||
limit,
|
||||
search: debouncedSearch || undefined,
|
||||
module_id: selectedModuleId || undefined,
|
||||
});
|
||||
setPrompts(result.data || []);
|
||||
setPagination({
|
||||
@ -86,7 +111,7 @@ const PromptManagement = (): ReactElement => {
|
||||
|
||||
useEffect(() => {
|
||||
void loadPrompts();
|
||||
}, [page, limit, debouncedSearch]);
|
||||
}, [page, limit, debouncedSearch, selectedModuleId]);
|
||||
|
||||
const handleStatusToggle = async (prompt: AIPrompt) => {
|
||||
const newStatus = prompt.status === "active" ? "draft" : "active";
|
||||
@ -313,13 +338,25 @@ const PromptManagement = (): ReactElement => {
|
||||
}}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<div className="pb-2">
|
||||
<div className="flex flex-col md:flex-row md:items-center gap-3 pb-2">
|
||||
<SearchBox
|
||||
value={search}
|
||||
onChange={setSearch}
|
||||
placeholder="Search by name or description..."
|
||||
containerClassName="relative w-full md:w-80"
|
||||
/>
|
||||
{/* <div className="w-full md:w-60"> */}
|
||||
<FilterDropdown
|
||||
label="Module"
|
||||
value={selectedModuleId || null}
|
||||
onChange={(val) => {
|
||||
setSelectedModuleId(val ? (Array.isArray(val) ? val[0] : val) : "");
|
||||
setPage(1);
|
||||
}}
|
||||
options={modules.map((m) => ({ value: m.value, label: m.label }))}
|
||||
placeholder="All Modules"
|
||||
/>
|
||||
{/* </div> */}
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
|
||||
@ -149,6 +149,7 @@ class AIService {
|
||||
variables?: Array<{ name: string; type?: "string" | "number" | "boolean" | "array"; required?: boolean }>;
|
||||
tags?: string[];
|
||||
is_default?: boolean;
|
||||
module_ids?: string[];
|
||||
}): Promise<AIPrompt> {
|
||||
const response = await apiClient.post("/ai/prompts", payload);
|
||||
return unwrap<AIPrompt>(response);
|
||||
@ -173,6 +174,7 @@ class AIService {
|
||||
tags?: string[];
|
||||
is_default?: boolean;
|
||||
change_notes?: string;
|
||||
module_ids?: string[];
|
||||
}): Promise<AIPrompt> {
|
||||
const response = await apiClient.put(`/ai/prompts/${id}`, payload);
|
||||
return unwrap<AIPrompt>(response);
|
||||
@ -183,7 +185,7 @@ class AIService {
|
||||
return unwrap<AIPrompt>(response);
|
||||
}
|
||||
|
||||
async listPrompts(params: { page?: number; limit?: number; status?: string; search?: string } = {}): Promise<{
|
||||
async listPrompts(params: { page?: number; limit?: number; status?: string; search?: string; module_id?: string } = {}): Promise<{
|
||||
data: AIPrompt[];
|
||||
pagination?: { page: number; limit: number; total: number; totalPages: number };
|
||||
}> {
|
||||
|
||||
@ -127,6 +127,8 @@ export interface AIPrompt {
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
created_by_email?:string;
|
||||
module_ids?: string[] | null;
|
||||
moduleIds?: string[];
|
||||
}
|
||||
|
||||
export interface KnowledgeCollection {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user