feat: implement AI service and prompt management pages for tenant configuration

This commit is contained in:
sibarchannayak 2026-05-29 18:01:31 +05:30
parent b16e6d9c18
commit 85ecd9dac1
5 changed files with 161 additions and 7 deletions

View File

@ -9,9 +9,11 @@ import {
SecondaryButton, SecondaryButton,
FormSlider, FormSlider,
FormTagInput, FormTagInput,
MultiselectPaginatedSelect,
} from "@/components/shared"; } from "@/components/shared";
import { Plus, Trash2 } from "lucide-react"; import { Plus, Trash2 } from "lucide-react";
import { aiService } from "@/services/ai-service"; import { aiService } from "@/services/ai-service";
import { moduleService } from "@/services/module-service";
import { showToast } from "@/utils/toast"; import { showToast } from "@/utils/toast";
import { useForm, useFieldArray, Controller } from "react-hook-form"; import { useForm, useFieldArray, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@ -39,6 +41,7 @@ const promptSchema = z.object({
tags: z.array(z.string()), tags: z.array(z.string()),
is_default: z.boolean(), is_default: z.boolean(),
variables: z.array(variableSchema), variables: z.array(variableSchema),
module_ids: z.array(z.string().uuid()).optional(),
}); });
type PromptFormData = z.infer<typeof promptSchema>; type PromptFormData = z.infer<typeof promptSchema>;
@ -47,6 +50,7 @@ const PromptCreate = (): ReactElement => {
const navigate = useNavigate(); const navigate = useNavigate();
const [isSubmitting, setIsSubmitting] = useState<boolean>(false); const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [apiProviders, setApiProviders] = useState<AIProviderInfo[]>([]); const [apiProviders, setApiProviders] = useState<AIProviderInfo[]>([]);
const [modules, setModules] = useState<Array<{ value: string; label: string }>>([]);
const { const {
control, control,
@ -76,6 +80,7 @@ const PromptCreate = (): ReactElement => {
default: "", default: "",
}, },
], ],
module_ids: [],
}, },
}); });
@ -100,19 +105,53 @@ const PromptCreate = (): ReactElement => {
useEffect(() => { useEffect(() => {
const loadMeta = async (): Promise<void> => { const loadMeta = async (): Promise<void> => {
try { try {
const providerData = await aiService.getProviders(); const [providerData, modulesRes] = await Promise.all([
aiService.getProviders(),
moduleService.getMyModules(),
]);
setApiProviders(providerData); setApiProviders(providerData);
setModules(
(modulesRes.data || []).map((m: any) => ({
value: m.id,
label: m.name,
}))
);
} catch (err: unknown) { } catch (err: unknown) {
showToast.error( showToast.error(
(err as { response?: { data?: { error?: { message?: string } } } }) (err as { response?: { data?: { error?: { message?: string } } } })
?.response?.data?.error?.message || ?.response?.data?.error?.message ||
"Failed to load provider metadata", "Failed to load provider or module metadata",
); );
} }
}; };
void loadMeta(); 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 onFormSubmit = async (data: PromptFormData): Promise<void> => {
const parsedTags = data.tags || []; const parsedTags = data.tags || [];
@ -140,6 +179,7 @@ const PromptCreate = (): ReactElement => {
variables: sanitizedVariables, variables: sanitizedVariables,
tags: parsedTags, tags: parsedTags,
is_default: data.is_default, is_default: data.is_default,
module_ids: data.module_ids,
}); });
showToast.success("Prompt created successfully"); showToast.success("Prompt created successfully");
navigate("/tenant/ai/prompts"); navigate("/tenant/ai/prompts");
@ -559,6 +599,23 @@ const PromptCreate = (): ReactElement => {
<h2 className="text-sm font-semibold text-gray-800"> <h2 className="text-sm font-semibold text-gray-800">
Organization Organization
</h2> </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 <Controller
name="tags" name="tags"
control={control} control={control}

View File

@ -8,10 +8,12 @@ import {
PrimaryButton, PrimaryButton,
SecondaryButton, SecondaryButton,
FormSlider, FormSlider,
FormTagInput FormTagInput,
MultiselectPaginatedSelect
} from "@/components/shared"; } from "@/components/shared";
import { Plus, Trash2 } from "lucide-react"; import { Plus, Trash2 } from "lucide-react";
import { aiService } from "@/services/ai-service"; import { aiService } from "@/services/ai-service";
import { moduleService } from "@/services/module-service";
import { showToast } from "@/utils/toast"; import { showToast } from "@/utils/toast";
import { useForm, useFieldArray, Controller } from "react-hook-form"; import { useForm, useFieldArray, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@ -40,6 +42,7 @@ const promptSchema = z.object({
is_default: z.boolean(), is_default: z.boolean(),
variables: z.array(variableSchema), variables: z.array(variableSchema),
change_notes: z.string().optional(), change_notes: z.string().optional(),
module_ids: z.array(z.string().uuid()).optional(),
}); });
type PromptFormData = z.infer<typeof promptSchema>; type PromptFormData = z.infer<typeof promptSchema>;
@ -50,6 +53,7 @@ const PromptEdit = (): ReactElement => {
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
const [isSubmitting, setIsSubmitting] = useState<boolean>(false); const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [apiProviders, setApiProviders] = useState<AIProviderInfo[]>([]); const [apiProviders, setApiProviders] = useState<AIProviderInfo[]>([]);
const [modules, setModules] = useState<Array<{ value: string; label: string }>>([]);
const [currentVersion, setCurrentVersion] = useState<number>(1); const [currentVersion, setCurrentVersion] = useState<number>(1);
const { const {
@ -75,6 +79,7 @@ const PromptEdit = (): ReactElement => {
is_default: false, is_default: false,
variables: [], variables: [],
change_notes: "", change_notes: "",
module_ids: [],
}, },
}); });
@ -101,12 +106,19 @@ const PromptEdit = (): ReactElement => {
if (!id) return; if (!id) return;
try { try {
setIsLoading(true); setIsLoading(true);
const [providerData, promptData] = await Promise.all([ const [providerData, promptData, modulesRes] = await Promise.all([
aiService.getProviders(), aiService.getProviders(),
aiService.getPrompt(id), aiService.getPrompt(id),
moduleService.getMyModules(),
]); ]);
setApiProviders(providerData); setApiProviders(providerData);
setModules(
(modulesRes.data || []).map((m: any) => ({
value: m.id,
label: m.name,
}))
);
reset({ reset({
name: promptData.name, name: promptData.name,
@ -127,6 +139,7 @@ const PromptEdit = (): ReactElement => {
default: v.default ? String(v.default) : "", default: v.default ? String(v.default) : "",
})), })),
change_notes: "", change_notes: "",
module_ids: promptData.moduleIds || [],
}); });
setCurrentVersion(promptData.version || 1); setCurrentVersion(promptData.version || 1);
} catch (err: unknown) { } catch (err: unknown) {
@ -143,6 +156,31 @@ const PromptEdit = (): ReactElement => {
void loadData(); void loadData();
}, [id, reset, navigate]); }, [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> => { const onFormSubmit = async (data: PromptFormData): Promise<void> => {
if (!id) return; if (!id) return;
const parsedTags = data.tags || []; const parsedTags = data.tags || [];
@ -172,6 +210,7 @@ const PromptEdit = (): ReactElement => {
tags: parsedTags, tags: parsedTags,
is_default: data.is_default, is_default: data.is_default,
change_notes: data.change_notes, change_notes: data.change_notes,
module_ids: data.module_ids,
}); });
showToast.success("Prompt updated successfully"); showToast.success("Prompt updated successfully");
navigate("/tenant/ai/prompts"); navigate("/tenant/ai/prompts");
@ -601,6 +640,23 @@ const PromptEdit = (): ReactElement => {
<h2 className="text-sm font-semibold text-gray-800"> <h2 className="text-sm font-semibold text-gray-800">
Organization Organization
</h2> </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 <Controller
name="tags" name="tags"
control={control} control={control}

View File

@ -19,8 +19,10 @@ import {
ActionDropdown, ActionDropdown,
DeleteConfirmationModal, DeleteConfirmationModal,
SearchBox, SearchBox,
FilterDropdown,
} from "@/components/shared"; } from "@/components/shared";
import { aiService } from "@/services/ai-service"; import { aiService } from "@/services/ai-service";
import { moduleService } from "@/services/module-service";
import type { AIPrompt } from "@/types/ai"; import type { AIPrompt } from "@/types/ai";
import { showToast } from "@/utils/toast"; import { showToast } from "@/utils/toast";
import { formatDate } from "@/utils/format-date"; import { formatDate } from "@/utils/format-date";
@ -43,6 +45,10 @@ const PromptManagement = (): ReactElement => {
totalPages: 1, totalPages: 1,
}); });
// Module filter states
const [modules, setModules] = useState<Array<{ value: string; label: string }>>([]);
const [selectedModuleId, setSelectedModuleId] = useState<string>("");
// Modal states // Modal states
const [selectedPrompt, setSelectedPrompt] = useState<AIPrompt | null>(null); const [selectedPrompt, setSelectedPrompt] = useState<AIPrompt | null>(null);
const [isVersionsOpen, setIsVersionsOpen] = useState(false); const [isVersionsOpen, setIsVersionsOpen] = useState(false);
@ -57,6 +63,24 @@ const PromptManagement = (): ReactElement => {
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [search]); }, [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> => { const loadPrompts = async (): Promise<void> => {
setIsLoading(true); setIsLoading(true);
setError(null); setError(null);
@ -65,6 +89,7 @@ const PromptManagement = (): ReactElement => {
page, page,
limit, limit,
search: debouncedSearch || undefined, search: debouncedSearch || undefined,
module_id: selectedModuleId || undefined,
}); });
setPrompts(result.data || []); setPrompts(result.data || []);
setPagination({ setPagination({
@ -86,7 +111,7 @@ const PromptManagement = (): ReactElement => {
useEffect(() => { useEffect(() => {
void loadPrompts(); void loadPrompts();
}, [page, limit, debouncedSearch]); }, [page, limit, debouncedSearch, selectedModuleId]);
const handleStatusToggle = async (prompt: AIPrompt) => { const handleStatusToggle = async (prompt: AIPrompt) => {
const newStatus = prompt.status === "active" ? "draft" : "active"; const newStatus = prompt.status === "active" ? "draft" : "active";
@ -313,13 +338,25 @@ const PromptManagement = (): ReactElement => {
}} }}
> >
<div className="overflow-hidden"> <div className="overflow-hidden">
<div className="pb-2"> <div className="flex flex-col md:flex-row md:items-center gap-3 pb-2">
<SearchBox <SearchBox
value={search} value={search}
onChange={setSearch} onChange={setSearch}
placeholder="Search by name or description..." placeholder="Search by name or description..."
containerClassName="relative w-full md:w-80" 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> </div>
<DataTable <DataTable

View File

@ -149,6 +149,7 @@ class AIService {
variables?: Array<{ name: string; type?: "string" | "number" | "boolean" | "array"; required?: boolean }>; variables?: Array<{ name: string; type?: "string" | "number" | "boolean" | "array"; required?: boolean }>;
tags?: string[]; tags?: string[];
is_default?: boolean; is_default?: boolean;
module_ids?: string[];
}): Promise<AIPrompt> { }): Promise<AIPrompt> {
const response = await apiClient.post("/ai/prompts", payload); const response = await apiClient.post("/ai/prompts", payload);
return unwrap<AIPrompt>(response); return unwrap<AIPrompt>(response);
@ -173,6 +174,7 @@ class AIService {
tags?: string[]; tags?: string[];
is_default?: boolean; is_default?: boolean;
change_notes?: string; change_notes?: string;
module_ids?: string[];
}): Promise<AIPrompt> { }): Promise<AIPrompt> {
const response = await apiClient.put(`/ai/prompts/${id}`, payload); const response = await apiClient.put(`/ai/prompts/${id}`, payload);
return unwrap<AIPrompt>(response); return unwrap<AIPrompt>(response);
@ -183,7 +185,7 @@ class AIService {
return unwrap<AIPrompt>(response); 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[]; data: AIPrompt[];
pagination?: { page: number; limit: number; total: number; totalPages: number }; pagination?: { page: number; limit: number; total: number; totalPages: number };
}> { }> {

View File

@ -127,6 +127,8 @@ export interface AIPrompt {
createdAt?: string; createdAt?: string;
updatedAt?: string; updatedAt?: string;
created_by_email?:string; created_by_email?:string;
module_ids?: string[] | null;
moduleIds?: string[];
} }
export interface KnowledgeCollection { export interface KnowledgeCollection {