feat(#82): implement Personality Module

- Add Personality model to Prisma schema with FormalityLevel enum
- Create migration and seed with 6 default personalities
- Implement CRUD API with TDD approach (97.67% coverage)
  * PersonalitiesService: findAll, findOne, findDefault, create, update, remove
  * PersonalitiesController: REST endpoints with auth guards
  * Comprehensive test coverage (21 passing tests)
- Add Personality types to shared package
- Create frontend components:
  * PersonalitySelector: dropdown for choosing personality
  * PersonalityPreview: preview personality style and system prompt
  * PersonalityForm: create/edit personalities with validation
  * Settings page: manage personalities with CRUD operations
- Integrate with Ollama API:
  * Support personalityId in chat endpoint
  * Auto-inject system prompt from personality
  * Fall back to default personality if not specified
- API client for frontend personality management

All tests passing with 97.67% backend coverage (exceeds 85% requirement)
This commit is contained in:
Jason Woltje
2026-01-29 17:57:54 -06:00
parent 95833fb4ea
commit 5dd46c85af
43 changed files with 4782 additions and 2 deletions

View File

@@ -0,0 +1,76 @@
"use client";
import { useState, useEffect } from "react";
import type { Personality } from "@mosaic/shared";
import { fetchPersonalities } from "@/lib/api/personalities";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
interface PersonalitySelectorProps {
value?: string;
onChange?: (personalityId: string) => void;
label?: string;
className?: string;
}
export function PersonalitySelector({
value,
onChange,
label = "Select Personality",
className,
}: PersonalitySelectorProps): JSX.Element {
const [personalities, setPersonalities] = useState<Personality[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
useEffect(() => {
loadPersonalities();
}, []);
async function loadPersonalities(): Promise<void> {
try {
setIsLoading(true);
const response = await fetchPersonalities();
setPersonalities(response.data);
} catch (err) {
console.error("Failed to load personalities:", err);
} finally {
setIsLoading(false);
}
}
return (
<div className={className}>
{label && (
<Label htmlFor="personality-select" className="mb-2">
{label}
</Label>
)}
<Select value={value} onValueChange={onChange} disabled={isLoading}>
<SelectTrigger id="personality-select">
<SelectValue placeholder={isLoading ? "Loading..." : "Choose a personality"} />
</SelectTrigger>
<SelectContent>
{personalities.map((personality) => (
<SelectItem key={personality.id} value={personality.id}>
<div className="flex items-center gap-2">
<span>{personality.name}</span>
{personality.isDefault && (
<Badge variant="secondary" className="ml-2">
Default
</Badge>
)}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
}