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,121 @@
"use client";
import { useState } from "react";
import type { Personality } from "@mosaic/shared";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge";
import { Sparkles } from "lucide-react";
interface PersonalityPreviewProps {
personality: Personality;
}
const SAMPLE_PROMPTS = [
"Explain quantum computing in simple terms",
"What's the best way to organize my tasks?",
"Help me brainstorm ideas for a new project",
];
const FORMALITY_LABELS: Record<string, string> = {
VERY_CASUAL: "Very Casual",
CASUAL: "Casual",
NEUTRAL: "Neutral",
FORMAL: "Formal",
VERY_FORMAL: "Very Formal",
};
export function PersonalityPreview({ personality }: PersonalityPreviewProps): JSX.Element {
const [selectedPrompt, setSelectedPrompt] = useState<string>(SAMPLE_PROMPTS[0]);
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<Sparkles className="h-5 w-5" />
{personality.name}
</CardTitle>
<CardDescription>{personality.description}</CardDescription>
</div>
{personality.isDefault && (
<Badge variant="secondary">Default</Badge>
)}
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Personality Attributes */}
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-muted-foreground">Tone:</span>
<Badge variant="outline" className="ml-2">
{personality.tone}
</Badge>
</div>
<div>
<span className="text-muted-foreground">Formality:</span>
<Badge variant="outline" className="ml-2">
{FORMALITY_LABELS[personality.formalityLevel]}
</Badge>
</div>
</div>
{/* Sample Interaction */}
<div className="space-y-2">
<label className="text-sm font-medium">Preview with Sample Prompt:</label>
<div className="flex flex-wrap gap-2">
{SAMPLE_PROMPTS.map((prompt) => (
<Button
key={prompt}
variant={selectedPrompt === prompt ? "default" : "outline"}
size="sm"
onClick={() => setSelectedPrompt(prompt)}
>
{prompt.substring(0, 30)}...
</Button>
))}
</div>
</div>
{/* System Prompt Template */}
<div className="space-y-2">
<label className="text-sm font-medium">System Prompt Template:</label>
<Textarea
value={personality.systemPromptTemplate}
readOnly
className="min-h-[100px] bg-muted"
/>
</div>
{/* Mock Response Preview */}
<div className="space-y-2">
<label className="text-sm font-medium">Sample Response Style:</label>
<div className="rounded-md border bg-muted/50 p-4 text-sm">
<p className="italic text-muted-foreground">
"{selectedPrompt}"
</p>
<div className="mt-2 text-foreground">
{personality.formalityLevel === "VERY_CASUAL" && (
<p>Hey! So quantum computing is like... imagine if your computer could be in multiple places at once. Pretty wild, right? 🤯</p>
)}
{personality.formalityLevel === "CASUAL" && (
<p>Sure! Think of quantum computing like a super-powered calculator that can try lots of solutions at the same time.</p>
)}
{personality.formalityLevel === "NEUTRAL" && (
<p>Quantum computing uses quantum mechanics principles to process information differently from classical computers, enabling parallel computation.</p>
)}
{personality.formalityLevel === "FORMAL" && (
<p>Quantum computing represents a paradigm shift in computational methodology, leveraging quantum mechanical phenomena to perform calculations.</p>
)}
{personality.formalityLevel === "VERY_FORMAL" && (
<p>Quantum computing constitutes a fundamental departure from classical computational architectures, employing quantum superposition and entanglement principles.</p>
)}
</div>
</div>
</div>
</CardContent>
</Card>
);
}