FRE-592: Implement character database and relationship mapping

Add full character management system with enriched profiles (bio, traits,
arcs, motivation, conflict, secrets), relationship mapping between
characters with types and strength, character search/filter by role and
arc type, and character statistics (scene count, dialogue, screen time).

Includes database schema, tRPC router procedures, SolidJS components,
API hooks, and unit tests.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
FrenoCorp Agent
2026-04-24 02:24:31 -04:00
committed by Michael Freno
parent 0fcd91cf87
commit 8dc4827597
18 changed files with 2237 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../../../server/trpc';
// Create tRPC client
export function createTRPCClientInstance(baseUrl: string = 'http://localhost:8080') {
return createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: `${baseUrl}/trpc`,
headers: () => {
// Add auth headers if available
const token = localStorage.getItem('auth_token');
return {
authorization: token ? `Bearer ${token}` : '',
};
},
}),
],
});
}
// Helper for SSR
export function createServerTRPCClient(baseUrl: string = 'http://localhost:8080') {
return createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: `${baseUrl}/trpc`,
}),
],
});
}

266
src/lib/api/trpc-hooks.ts Normal file
View File

@@ -0,0 +1,266 @@
import { createQuery, createMutation, useQueryClient } from '@tanstack/solid-query';
import { createTRPCClientInstance } from './trpc-client';
import type { Project, Character, CharacterRelationship, CharacterStats, Scene } from '../../../server/types/project';
const trpcClient = createTRPCClientInstance();
// Project hooks
export function useProjects() {
return createQuery({
queryKey: ['projects'],
queryFn: async () => {
const result = await trpcClient.project.listProjects.query();
return result as Project[];
},
});
}
export function useProject(projectId: string) {
return createQuery({
queryKey: ['project', projectId],
queryFn: async () => {
const result = await trpcClient.project.getProject.query({ id: projectId });
return result as Project;
},
enabled: !!projectId,
});
}
export function useCreateProject() {
return createMutation({
mutationFn: async (input: { name: string; description?: string }) => {
return trpcClient.project.createProject.mutate(input) as Promise<Project>;
},
onSuccess: () => {
const qc = useQueryClient();
qc.invalidateQueries({ queryKey: ['projects'] });
},
});
}
// Character hooks
export function useCharacters(projectId: string) {
return createQuery({
queryKey: ['characters', projectId],
queryFn: async () => {
const result = await trpcClient.project.listCharacters.query({ projectId });
return result as Character[];
},
enabled: !!projectId,
});
}
export function useCharacter(characterId: string) {
return createQuery({
queryKey: ['character', characterId],
queryFn: async () => {
const result = await trpcClient.project.getCharacter.query({ id: characterId });
return result as Character;
},
enabled: !!characterId,
});
}
export function useSearchCharacters(projectId: string, query?: string, role?: string, arcType?: string) {
return createQuery({
queryKey: ['searchCharacters', projectId, query, role, arcType],
queryFn: async () => {
const result = await trpcClient.project.searchCharacters.query({
projectId,
query,
role,
arcType,
});
return result as Character[];
},
enabled: !!projectId,
});
}
export function useCharacterStats(characterId: string) {
return createQuery({
queryKey: ['characterStats', characterId],
queryFn: async () => {
const result = await trpcClient.project.getCharacterStats.query({ characterId });
return result as CharacterStats;
},
enabled: !!characterId,
});
}
export function useProjectCharacterStats(projectId: string) {
return createQuery({
queryKey: ['projectCharacterStats', projectId],
queryFn: async () => {
const result = await trpcClient.project.getProjectCharacterStats.query({ projectId });
return result as CharacterStats[];
},
enabled: !!projectId,
});
}
export function useCreateCharacter() {
const qc = useQueryClient();
return createMutation({
mutationFn: async (input: {
name: string;
description?: string;
bio?: string;
role?: 'protagonist' | 'antagonist' | 'supporting' | 'background' | 'ensemble';
arc?: string;
arcType?: 'positive' | 'negative' | 'flat' | 'complex';
age?: number;
gender?: string;
voice?: string;
traits?: string;
motivation?: string;
conflict?: string;
secret?: string;
imageUrl?: string;
projectId: string;
}) => {
return trpcClient.project.createCharacter.mutate(input) as Promise<Character>;
},
onSuccess: (_, variables) => {
qc.invalidateQueries({ queryKey: ['characters', variables.projectId] });
},
});
}
export function useUpdateCharacter() {
const qc = useQueryClient();
return createMutation({
mutationFn: async (input: {
id: string;
name?: string;
description?: string;
bio?: string;
role?: 'protagonist' | 'antagonist' | 'supporting' | 'background' | 'ensemble';
arc?: string;
arcType?: 'positive' | 'negative' | 'flat' | 'complex';
age?: number;
gender?: string;
voice?: string;
traits?: string;
motivation?: string;
conflict?: string;
secret?: string;
imageUrl?: string;
projectId?: string;
}) => {
return trpcClient.project.updateCharacter.mutate(input) as Promise<Character>;
},
onSuccess: (_, variables) => {
qc.invalidateQueries({ queryKey: ['character', variables.id] });
qc.invalidateQueries({ queryKey: ['characters'] });
},
});
}
export function useDeleteCharacter() {
const qc = useQueryClient();
return createMutation({
mutationFn: async (id: string) => {
return trpcClient.project.deleteCharacter.mutate({ id }) as Promise<{ success: boolean }>;
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['characters'] });
qc.invalidateQueries({ queryKey: ['characterRelationships'] });
},
});
}
// Relationship hooks
export function useRelationships(projectId: string) {
return createQuery({
queryKey: ['characterRelationships', projectId],
queryFn: async () => {
const result = await trpcClient.project.listRelationships.query({ projectId });
return result as CharacterRelationship[];
},
enabled: !!projectId,
});
}
export function useCharacterRelationships(characterId: string) {
return createQuery({
queryKey: ['characterRelationships', characterId],
queryFn: async () => {
const result = await trpcClient.project.getRelationshipsForCharacter.query({ characterId });
return result as CharacterRelationship[];
},
enabled: !!characterId,
});
}
export function useCreateRelationship() {
const qc = useQueryClient();
return createMutation({
mutationFn: async (input: {
characterIdA: string;
characterIdB: string;
relationshipType: 'family' | 'romantic' | 'friendship' | 'rivalry' | 'mentor' | 'alliance' | 'conflict' | 'professional' | 'other';
description?: string;
strength?: number;
isAntagonistic?: boolean;
}) => {
return trpcClient.project.createRelationship.mutate(input) as Promise<CharacterRelationship>;
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['characterRelationships'] });
},
});
}
export function useUpdateRelationship() {
const qc = useQueryClient();
return createMutation({
mutationFn: async (input: {
id: string;
relationshipType?: 'family' | 'romantic' | 'friendship' | 'rivalry' | 'mentor' | 'alliance' | 'conflict' | 'professional' | 'other';
description?: string;
strength?: number;
isAntagonistic?: boolean;
}) => {
return trpcClient.project.updateRelationship.mutate(input) as Promise<CharacterRelationship>;
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['characterRelationships'] });
},
});
}
export function useDeleteRelationship() {
const qc = useQueryClient();
return createMutation({
mutationFn: async (id: string) => {
return trpcClient.project.deleteRelationship.mutate({ id }) as Promise<{ success: boolean }>;
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['characterRelationships'] });
},
});
}
// Scene hooks
export function useScenes(projectId: string) {
return createQuery({
queryKey: ['scenes', projectId],
queryFn: async () => {
const result = await trpcClient.project.listScenes.query({ projectId });
return result as Scene[];
},
enabled: !!projectId,
});
}
export function useScene(sceneId: string) {
return createQuery({
queryKey: ['scene', sceneId],
queryFn: async () => {
const result = await trpcClient.project.getScene.query({ id: sceneId });
return result as Scene;
},
enabled: !!sceneId,
});
}