641 lines
No EOL
23 KiB
TypeScript
641 lines
No EOL
23 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
import { useApiRequest } from './useApi';
|
|
import api from '../api';
|
|
import { usePermissions, type UserPermissions } from './usePermissions';
|
|
import {
|
|
fetchPrompts as fetchPromptsApi,
|
|
fetchPromptById as fetchPromptByIdApi,
|
|
createPrompt as createPromptApi,
|
|
updatePrompt as updatePromptApi,
|
|
deletePrompt as deletePromptApi,
|
|
type Prompt,
|
|
type UpdatePromptData,
|
|
type AttributeDefinition,
|
|
type PaginationParams
|
|
} from '../api/promptApi';
|
|
|
|
// Re-export types for backward compatibility
|
|
export type { Prompt, AttributeDefinition, PaginationParams };
|
|
|
|
// Re-export AttributeOption for backward compatibility
|
|
export interface AttributeOption {
|
|
value: string | number;
|
|
label: string;
|
|
}
|
|
|
|
// Prompts list hook
|
|
export function usePrompts() {
|
|
const [prompts, setPrompts] = useState<Prompt[]>([]);
|
|
const [attributes, setAttributes] = useState<AttributeDefinition[]>([]);
|
|
const [permissions, setPermissions] = useState<UserPermissions | null>(null);
|
|
const [pagination, setPagination] = useState<{
|
|
currentPage: number;
|
|
pageSize: number;
|
|
totalItems: number;
|
|
totalPages: number;
|
|
} | null>(null);
|
|
const [groupLayout, setGroupLayout] = useState<import('../api/connectionApi').GroupLayout | null>(null);
|
|
const [appliedView, setAppliedView] = useState<{ viewKey?: string; displayName?: string } | null>(null);
|
|
const { request, isLoading: loading, error } = useApiRequest<null, Prompt[]>();
|
|
const { checkPermission } = usePermissions();
|
|
|
|
// Fetch attributes from backend
|
|
const fetchAttributes = useCallback(async () => {
|
|
try {
|
|
const response = await api.get('/api/attributes/Prompt');
|
|
|
|
// Extract attributes from response - check if response.data.attributes exists, otherwise check if response.data is an array
|
|
let attrs: AttributeDefinition[] = [];
|
|
if (response.data?.attributes && Array.isArray(response.data.attributes)) {
|
|
attrs = response.data.attributes;
|
|
} else if (Array.isArray(response.data)) {
|
|
attrs = response.data;
|
|
} else if (response.data && typeof response.data === 'object') {
|
|
// Try to find any array property in the response
|
|
const keys = Object.keys(response.data);
|
|
for (const key of keys) {
|
|
if (Array.isArray(response.data[key])) {
|
|
attrs = response.data[key];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
setAttributes(attrs);
|
|
return attrs;
|
|
} catch (error: any) {
|
|
console.error('Error fetching attributes:', error);
|
|
setAttributes([]);
|
|
return [];
|
|
}
|
|
}, []);
|
|
|
|
// Fetch permissions from backend
|
|
const fetchPermissions = useCallback(async () => {
|
|
try {
|
|
const perms = await checkPermission('DATA', 'Prompt');
|
|
setPermissions(perms);
|
|
return perms;
|
|
} catch (error: any) {
|
|
console.error('Error fetching permissions:', error);
|
|
const defaultPerms: UserPermissions = {
|
|
view: false,
|
|
read: 'n',
|
|
create: 'n',
|
|
update: 'n',
|
|
delete: 'n',
|
|
};
|
|
setPermissions(defaultPerms);
|
|
return defaultPerms;
|
|
}
|
|
}, [checkPermission]);
|
|
|
|
const fetchGroupSectionSummaries = useCallback(
|
|
async (base: {
|
|
search?: string;
|
|
filters?: Record<string, any>;
|
|
sort?: Array<{ field: string; direction: string }>;
|
|
viewKey?: string | null;
|
|
groupField: string;
|
|
groupDirection?: 'asc' | 'desc';
|
|
}) => {
|
|
const pObj: Record<string, unknown> = {
|
|
page: 1,
|
|
pageSize: 25,
|
|
groupByLevels: [
|
|
{
|
|
field: base.groupField,
|
|
nullLabel: '—',
|
|
direction: base.groupDirection || 'asc',
|
|
},
|
|
],
|
|
};
|
|
if (base.search) (pObj as { search?: string }).search = base.search;
|
|
if (base.filters && Object.keys(base.filters).length) (pObj as { filters: typeof base.filters }).filters = base.filters;
|
|
if (base.sort?.length) (pObj as { sort: typeof base.sort }).sort = base.sort;
|
|
if (base.viewKey) (pObj as { viewKey: string }).viewKey = base.viewKey;
|
|
const { data } = await api.get('/api/prompts', {
|
|
params: { mode: 'groupSummary', pagination: JSON.stringify(pObj) },
|
|
});
|
|
return Array.isArray(data?.groups) ? data.groups : [];
|
|
},
|
|
[],
|
|
);
|
|
|
|
const refetchForSection = useCallback(
|
|
async (
|
|
paginationParams: any,
|
|
sectionFilter: Record<string, unknown>,
|
|
parentColumnFilters?: Record<string, unknown>,
|
|
) => {
|
|
const mergedFilters = {
|
|
...(parentColumnFilters || {}),
|
|
...(paginationParams.filters || {}),
|
|
...sectionFilter,
|
|
};
|
|
const pObj: Record<string, unknown> = {
|
|
page: paginationParams.page,
|
|
pageSize: paginationParams.pageSize,
|
|
filters: mergedFilters,
|
|
groupByLevels: [],
|
|
};
|
|
if (paginationParams.sort) (pObj as { sort: unknown }).sort = paginationParams.sort;
|
|
if (paginationParams.search) (pObj as { search: string }).search = paginationParams.search;
|
|
if (paginationParams.viewKey) (pObj as { viewKey: string }).viewKey = paginationParams.viewKey;
|
|
const { data } = await api.get('/api/prompts', {
|
|
params: { pagination: JSON.stringify(pObj) },
|
|
});
|
|
if (data && typeof data === 'object' && 'items' in data) {
|
|
return { items: data.items, pagination: data.pagination };
|
|
}
|
|
return { items: [], pagination: null };
|
|
},
|
|
[],
|
|
);
|
|
|
|
const fetchPrompts = useCallback(async (params?: PaginationParams) => {
|
|
try {
|
|
const data = await fetchPromptsApi(request, params);
|
|
|
|
// Handle paginated response
|
|
if (data && typeof data === 'object' && 'items' in data) {
|
|
const items = Array.isArray(data.items) ? data.items : [];
|
|
setPrompts(items);
|
|
if (data.pagination) {
|
|
setPagination(data.pagination);
|
|
}
|
|
setGroupLayout(data.groupLayout ?? null);
|
|
setAppliedView(data.appliedView ?? null);
|
|
} else {
|
|
// Handle non-paginated response (backward compatibility)
|
|
const items = Array.isArray(data) ? data : [];
|
|
setPrompts(items);
|
|
setPagination(null);
|
|
setGroupLayout(null);
|
|
setAppliedView(null);
|
|
}
|
|
} catch (error: any) {
|
|
// Error is already handled by useApiRequest
|
|
setPrompts([]);
|
|
setPagination(null);
|
|
setGroupLayout(null);
|
|
setAppliedView(null);
|
|
}
|
|
}, [request]);
|
|
|
|
// Optimistically remove a prompt from the local state
|
|
const removeOptimistically = (promptId: string) => {
|
|
setPrompts(prevPrompts => prevPrompts.filter(prompt => prompt.id !== promptId));
|
|
};
|
|
|
|
// Optimistically update a prompt in the local state
|
|
const updateOptimistically = (promptId: string, updateData: { name: string; content: string }) => {
|
|
setPrompts(prevPrompts =>
|
|
prevPrompts.map(prompt =>
|
|
prompt.id === promptId
|
|
? { ...prompt, ...updateData }
|
|
: prompt
|
|
)
|
|
);
|
|
};
|
|
|
|
// Fetch a single prompt by ID
|
|
const fetchPromptById = useCallback(async (promptId: string): Promise<Prompt | null> => {
|
|
return await fetchPromptByIdApi(request, promptId);
|
|
}, [request]);
|
|
|
|
// Generate edit fields from attributes dynamically
|
|
const generateEditFieldsFromAttributes = useCallback((): Array<{
|
|
key: string;
|
|
label: string;
|
|
type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly';
|
|
editable?: boolean;
|
|
required?: boolean;
|
|
validator?: (value: any) => string | null;
|
|
minRows?: number;
|
|
maxRows?: number;
|
|
options?: Array<{ value: string | number; label: string }>;
|
|
optionsReference?: string; // For options that need to be fetched (e.g., "user.role")
|
|
}> => {
|
|
if (!attributes || attributes.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const editableFields = attributes
|
|
.filter(attr => {
|
|
// Filter out non-editable fields based on readonly/editable flags
|
|
if (attr.readonly === true || attr.editable === false) {
|
|
return false; // Don't show readonly fields in edit form
|
|
}
|
|
// Also filter out common non-editable fields
|
|
const nonEditableFields = ['id', 'mandateId', 'sysCreatedBy', '_hideDelete'];
|
|
return !nonEditableFields.includes(attr.name);
|
|
})
|
|
.map(attr => {
|
|
// Check if this is a content field first - check this BEFORE type mapping
|
|
const isContentField = attr.name === 'content' ||
|
|
attr.name.toLowerCase().includes('content') ||
|
|
attr.name.toLowerCase().includes('description');
|
|
|
|
// Map backend attribute type to form field type
|
|
// IMPORTANT: Check for content fields FIRST before other type checks
|
|
let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' = 'string';
|
|
let options: Array<{ value: string | number; label: string }> | undefined = undefined;
|
|
let optionsReference: string | undefined = undefined;
|
|
|
|
if (isContentField) {
|
|
// Force content fields to ALWAYS be textarea, regardless of attribute type
|
|
fieldType = 'textarea';
|
|
} else if (attr.type === 'checkbox') {
|
|
fieldType = 'boolean';
|
|
} else if (attr.type === 'email') {
|
|
fieldType = 'email';
|
|
} else if (attr.type === 'date') {
|
|
fieldType = 'date';
|
|
} else if (attr.type === 'select') {
|
|
fieldType = 'enum';
|
|
// Handle options - can be array or string reference
|
|
if (Array.isArray(attr.options)) {
|
|
options = attr.options.map(opt => ({
|
|
value: opt.value,
|
|
label: String(opt.label ?? opt.value)
|
|
}));
|
|
} else if (typeof attr.options === 'string') {
|
|
optionsReference = attr.options;
|
|
}
|
|
} else if (attr.type === 'multiselect') {
|
|
fieldType = 'multiselect';
|
|
// Handle options - can be array or string reference
|
|
if (Array.isArray(attr.options)) {
|
|
options = attr.options.map(opt => ({
|
|
value: opt.value,
|
|
label: String(opt.label ?? opt.value)
|
|
}));
|
|
} else if (typeof attr.options === 'string') {
|
|
optionsReference = attr.options;
|
|
}
|
|
} else if (attr.type === 'textarea') {
|
|
fieldType = 'textarea';
|
|
} else if (attr.type === 'text') {
|
|
// Check if it should be textarea based on name
|
|
if (attr.name.toLowerCase().includes('text') || attr.name.toLowerCase().includes('note')) {
|
|
fieldType = 'textarea';
|
|
} else {
|
|
fieldType = 'string';
|
|
}
|
|
}
|
|
// Note: Legacy 'boolean' and 'enum' types are not in the AttributeDefinition type union
|
|
// If needed, they should be handled via type casting: (attr as any).type === 'boolean'
|
|
|
|
// Define validators and required fields
|
|
let required = attr.required === true;
|
|
let validator: ((value: any) => string | null) | undefined = undefined;
|
|
let minRows: number | undefined = undefined;
|
|
let maxRows: number | undefined = undefined;
|
|
|
|
// Match create button configuration for prompts
|
|
if (attr.name === 'name') {
|
|
required = true;
|
|
validator = (value: any) => {
|
|
if (!value || (typeof value === 'string' && value.trim() === '')) {
|
|
return 'Prompt name cannot be empty';
|
|
}
|
|
if (typeof value === 'string' && value.length > 100) {
|
|
return 'Prompt name cannot exceed 100 characters';
|
|
}
|
|
return null;
|
|
};
|
|
} else if (isContentField || attr.name === 'content') {
|
|
required = true;
|
|
minRows = 6; // Match create button: minRows: 6
|
|
maxRows = 12; // Match create button: maxRows: 12
|
|
validator = (value: any) => {
|
|
if (!value || (typeof value === 'string' && value.trim() === '')) {
|
|
return 'Prompt content cannot be empty';
|
|
}
|
|
if (typeof value === 'string' && value.length > 10000) {
|
|
return 'Prompt content cannot exceed 10,000 characters';
|
|
}
|
|
return null;
|
|
};
|
|
} else if (fieldType === 'textarea') {
|
|
// Default textarea settings for other textarea fields
|
|
minRows = 4;
|
|
maxRows = 8;
|
|
}
|
|
// Multiselect validation
|
|
else if (fieldType === 'multiselect' && required) {
|
|
validator = (value: any[]) => {
|
|
if (!value || !Array.isArray(value) || value.length === 0) {
|
|
return `${attr.label} is required`;
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
|
|
return {
|
|
key: attr.name,
|
|
label: attr.label || attr.name,
|
|
type: fieldType,
|
|
editable: attr.editable !== false && attr.readonly !== true,
|
|
required,
|
|
validator,
|
|
minRows,
|
|
maxRows,
|
|
options,
|
|
optionsReference
|
|
};
|
|
});
|
|
|
|
// Ensure we always return at least the basic fields if attributes exist
|
|
// This handles cases where all fields might be filtered out
|
|
if (editableFields.length === 0 && attributes.length > 0) {
|
|
// If all fields were filtered out, include all attributes as editable string fields
|
|
// Match create button configuration
|
|
return attributes.map(attr => {
|
|
const isContentField = attr.name === 'content' || attr.name.toLowerCase().includes('content');
|
|
const fieldType = isContentField ? 'textarea' as const : 'string' as const;
|
|
|
|
let required = false;
|
|
let validator: ((value: any) => string | null) | undefined = undefined;
|
|
let minRows: number | undefined = undefined;
|
|
let maxRows: number | undefined = undefined;
|
|
|
|
if (attr.name === 'name') {
|
|
required = true;
|
|
validator = (value: any) => {
|
|
if (!value || (typeof value === 'string' && value.trim() === '')) {
|
|
return 'Prompt name cannot be empty';
|
|
}
|
|
if (typeof value === 'string' && value.length > 100) {
|
|
return 'Prompt name cannot exceed 100 characters';
|
|
}
|
|
return null;
|
|
};
|
|
} else if (isContentField) {
|
|
required = true;
|
|
minRows = 6; // Match create button
|
|
maxRows = 12; // Match create button
|
|
validator = (value: any) => {
|
|
if (!value || (typeof value === 'string' && value.trim() === '')) {
|
|
return 'Prompt content cannot be empty';
|
|
}
|
|
if (typeof value === 'string' && value.length > 10000) {
|
|
return 'Prompt content cannot exceed 10,000 characters';
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
|
|
return {
|
|
key: attr.name,
|
|
label: attr.label || attr.name,
|
|
type: fieldType,
|
|
editable: true,
|
|
required,
|
|
validator,
|
|
minRows,
|
|
maxRows
|
|
};
|
|
});
|
|
}
|
|
|
|
return editableFields;
|
|
}, [attributes]);
|
|
|
|
// Generate create fields from attributes dynamically
|
|
// For prompts, the create form is essentially the same as edit form
|
|
const generateCreateFieldsFromAttributes = useCallback((): Array<{
|
|
key: string;
|
|
label: string;
|
|
type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly';
|
|
required?: boolean;
|
|
validator?: (value: any) => string | null;
|
|
minRows?: number;
|
|
maxRows?: number;
|
|
options?: Array<{ value: string | number; label: string }>;
|
|
optionsReference?: string;
|
|
placeholder?: string;
|
|
}> => {
|
|
if (!attributes || attributes.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const createFields = attributes
|
|
.filter(attr => {
|
|
// Filter out non-editable fields and auto-generated fields for create forms
|
|
if (attr.readonly === true || attr.editable === false) {
|
|
return false;
|
|
}
|
|
// Filter out ID fields and other auto-generated fields
|
|
const nonEditableFields = ['id', 'mandateId', 'sysCreatedBy', '_hideDelete'];
|
|
return !nonEditableFields.includes(attr.name);
|
|
})
|
|
.map(attr => {
|
|
// Map backend attribute type to form field type
|
|
let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' = 'string';
|
|
let minRows: number | undefined = undefined;
|
|
let maxRows: number | undefined = undefined;
|
|
|
|
// Map backend types to form field types
|
|
// Cast to string to handle all possible backend type values
|
|
const attrType = attr.type as string;
|
|
if (attrType === 'checkbox' || attrType === 'boolean') {
|
|
fieldType = 'boolean';
|
|
} else if (attrType === 'email') {
|
|
fieldType = 'email';
|
|
} else if (attrType === 'timestamp' || attrType === 'date' || attrType === 'time') {
|
|
fieldType = 'date';
|
|
} else if (attrType === 'textarea') {
|
|
fieldType = 'textarea';
|
|
// Set default rows for textarea fields
|
|
minRows = 6;
|
|
maxRows = 12;
|
|
} else if (attr.name === 'content' || attr.name.toLowerCase().includes('content')) {
|
|
// Content fields should be textarea
|
|
fieldType = 'textarea';
|
|
minRows = 6;
|
|
maxRows = 12;
|
|
}
|
|
|
|
// Determine if required and build validator
|
|
const required = attr.required === true;
|
|
let validator: ((value: any) => string | null) | undefined = undefined;
|
|
|
|
// Required string validation
|
|
if (required && (fieldType === 'string' || fieldType === 'textarea')) {
|
|
validator = (value: any) => {
|
|
if (!value || (typeof value === 'string' && value.trim() === '')) {
|
|
return `${attr.label} is required`;
|
|
}
|
|
if (attr.name === 'name' && typeof value === 'string' && value.length > 100) {
|
|
return 'Prompt name cannot exceed 100 characters';
|
|
}
|
|
if (attr.name === 'content' && typeof value === 'string' && value.length > 10000) {
|
|
return 'Prompt content cannot exceed 10,000 characters';
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
|
|
return {
|
|
key: attr.name,
|
|
label: attr.label || attr.name,
|
|
type: fieldType,
|
|
required,
|
|
validator,
|
|
minRows,
|
|
maxRows
|
|
};
|
|
});
|
|
|
|
return createFields;
|
|
}, [attributes]);
|
|
|
|
// Ensure attributes are loaded - can be called by EditActionButton
|
|
const ensureAttributesLoaded = useCallback(async () => {
|
|
// If attributes are already loaded, return them
|
|
if (attributes && attributes.length > 0) {
|
|
return attributes;
|
|
}
|
|
|
|
// Otherwise, fetch them and return the result
|
|
const fetchedAttributes = await fetchAttributes();
|
|
return fetchedAttributes;
|
|
}, [attributes, fetchAttributes]);
|
|
|
|
// Fetch attributes and permissions on mount
|
|
useEffect(() => {
|
|
fetchAttributes();
|
|
fetchPermissions();
|
|
}, [fetchAttributes, fetchPermissions]);
|
|
|
|
// Initial fetch
|
|
useEffect(() => {
|
|
fetchPrompts();
|
|
}, [fetchPrompts]);
|
|
|
|
return {
|
|
prompts,
|
|
loading,
|
|
error,
|
|
refetch: fetchPrompts,
|
|
removeOptimistically,
|
|
updateOptimistically,
|
|
attributes,
|
|
permissions,
|
|
pagination,
|
|
groupLayout,
|
|
appliedView,
|
|
fetchPromptById,
|
|
generateEditFieldsFromAttributes,
|
|
generateCreateFieldsFromAttributes,
|
|
ensureAttributesLoaded,
|
|
fetchGroupSectionSummaries,
|
|
refetchForSection,
|
|
};
|
|
}
|
|
|
|
// Prompt operations hook
|
|
export function usePromptOperations() {
|
|
const [deletingPrompts, setDeletingPrompts] = useState<Set<string>>(new Set());
|
|
const [creatingPrompt, setCreatingPrompt] = useState(false);
|
|
const { request, isLoading } = useApiRequest();
|
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
const [createError, setCreateError] = useState<string | null>(null);
|
|
const [updateError, setUpdateError] = useState<string | null>(null);
|
|
|
|
const handlePromptDelete = async (promptId: string) => {
|
|
setDeleteError(null);
|
|
setDeletingPrompts(prev => new Set(prev).add(promptId));
|
|
|
|
try {
|
|
await deletePromptApi(request, promptId);
|
|
|
|
// Add a small delay to ensure backend has time to process
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
return true;
|
|
} catch (error: any) {
|
|
setDeleteError(error.message);
|
|
return false;
|
|
} finally {
|
|
setDeletingPrompts(prev => {
|
|
const newSet = new Set(prev);
|
|
newSet.delete(promptId);
|
|
return newSet;
|
|
});
|
|
}
|
|
};
|
|
|
|
const handlePromptCreate = async (promptData: { name: string; content: string }) => {
|
|
setCreateError(null);
|
|
setCreatingPrompt(true);
|
|
|
|
try {
|
|
// mandateId wird nicht mehr vom Client gesendet
|
|
// Das Backend bestimmt den Kontext über die instanceId im Request
|
|
const requestBody = {
|
|
name: promptData.name,
|
|
content: promptData.content
|
|
};
|
|
|
|
const newPrompt = await createPromptApi(request, requestBody);
|
|
|
|
return { success: true, promptData: newPrompt };
|
|
} catch (error: any) {
|
|
setCreateError(error.message);
|
|
return { success: false, error: error.message };
|
|
} finally {
|
|
setCreatingPrompt(false);
|
|
}
|
|
};
|
|
|
|
const handlePromptUpdate = async (promptId: string, updateData: Record<string, any>, _originalData?: any) => {
|
|
setUpdateError(null);
|
|
|
|
try {
|
|
// Pass all provided fields (supports partial inline updates like isSystem toggle)
|
|
const { id, mandateId, sysCreatedBy, sysCreatedAt, sysModifiedAt, _permissions, ...requestBody } = updateData;
|
|
|
|
const updatedPrompt = await updatePromptApi(request, promptId, requestBody as UpdatePromptData);
|
|
|
|
return { success: true, promptData: updatedPrompt };
|
|
} catch (error: any) {
|
|
const errorMessage = error.response?.data?.message || error.message || 'Failed to update prompt';
|
|
const statusCode = error.response?.status;
|
|
|
|
setUpdateError(errorMessage);
|
|
|
|
// Return detailed error information for proper handling
|
|
return {
|
|
success: false,
|
|
error: errorMessage,
|
|
statusCode,
|
|
isPermissionError: statusCode === 403,
|
|
isValidationError: statusCode === 400
|
|
};
|
|
}
|
|
};
|
|
|
|
// Generic inline update handler for FormGeneratorTable
|
|
const handleInlineUpdate = async (promptId: string, changes: Record<string, any>) => {
|
|
const result = await handlePromptUpdate(promptId, changes);
|
|
if (!result.success) {
|
|
throw new Error(result.error || 'Failed to update');
|
|
}
|
|
return result;
|
|
};
|
|
|
|
return {
|
|
deletingPrompts,
|
|
creatingPrompt,
|
|
deleteError,
|
|
createError,
|
|
updateError,
|
|
handlePromptDelete,
|
|
handlePromptCreate,
|
|
handlePromptUpdate,
|
|
handleInlineUpdate,
|
|
isLoading
|
|
};
|
|
}
|