Phase 1 complete

This commit is contained in:
GW_MC
2026-02-24 10:34:55 +00:00
parent 630b60d7e2
commit 8714d6bd22
112 changed files with 11063 additions and 73 deletions

View File

@@ -0,0 +1,131 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { authControllerLogin } from '../api/generated/auth/auth';
import type { LoginDto } from '../api/generated/model';
interface User {
id: string;
email: string;
username: string;
role: string;
}
interface AuthState {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
error: string | null;
// Actions
login: (credentials: LoginDto) => Promise<void>;
setTokens: (accessToken: string, refreshToken: string) => void;
logout: () => void;
clearError: () => void;
init: () => void;
}
// Parse JWT token to get user info
const parseJwt = (token: string): { sub: string; email: string; role: string } | null => {
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join(''),
);
return JSON.parse(jsonPayload);
} catch {
return null;
}
};
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
user: null,
isAuthenticated: false,
isLoading: false,
error: null,
init: () => {
const token = localStorage.getItem('accessToken');
if (token) {
const payload = parseJwt(token);
if (payload) {
set({
user: {
id: payload.sub,
email: payload.email,
username: payload.email.split('@')[0],
role: payload.role,
},
isAuthenticated: true,
});
} else {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
}
}
},
login: async (credentials) => {
set({ isLoading: true, error: null });
try {
const response = await authControllerLogin(credentials);
localStorage.setItem('accessToken', response.accessToken);
localStorage.setItem('refreshToken', response.refreshToken);
set({
user: response.user,
isAuthenticated: true,
isLoading: false,
});
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Login failed',
isLoading: false,
});
throw error;
}
},
setTokens: (accessToken, refreshToken) => {
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
const payload = parseJwt(accessToken);
if (payload) {
set({
user: {
id: payload.sub,
email: payload.email,
username: payload.email.split('@')[0],
role: payload.role,
},
isAuthenticated: true,
});
}
},
logout: () => {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
set({
user: null,
isAuthenticated: false,
error: null,
});
},
clearError: () => set({ error: null }),
}),
{
name: 'auth-storage',
partialize: (state) => ({
user: state.user,
isAuthenticated: state.isAuthenticated,
}),
},
),
);

View File

@@ -0,0 +1,120 @@
import { create } from 'zustand';
import {
characterControllerCreate,
characterControllerFindAll,
characterControllerFindOne,
characterControllerUpdate,
characterControllerDelete,
} from '../api/generated/characters/characters';
import type { CreateCharacterDto, UpdateCharacterDto } from '../api/generated/model';
import type { Character } from '../types';
interface CharacterState {
characters: Character[];
currentCharacter: Character | null;
isLoading: boolean;
error: string | null;
// Actions
fetchCharacters: () => Promise<void>;
getCharacter: (id: string) => Promise<Character>;
createCharacter: (data: CreateCharacterDto) => Promise<Character>;
updateCharacter: (id: string, data: UpdateCharacterDto) => Promise<Character>;
deleteCharacter: (id: string) => Promise<void>;
setCurrentCharacter: (character: Character | null) => void;
clearError: () => void;
}
export const useCharacterStore = create<CharacterState>()((set) => ({
characters: [],
currentCharacter: null,
isLoading: false,
error: null,
fetchCharacters: async () => {
set({ isLoading: true, error: null });
try {
// The generated API returns void, we need to cast the response
const characters = await characterControllerFindAll() as unknown as Character[];
set({ characters, isLoading: false });
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to fetch characters',
isLoading: false,
});
}
},
getCharacter: async (id) => {
set({ isLoading: true, error: null });
try {
const character = await characterControllerFindOne(id) as unknown as Character;
set({ currentCharacter: character, isLoading: false });
return character;
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to fetch character',
isLoading: false,
});
throw error;
}
},
createCharacter: async (data) => {
set({ isLoading: true, error: null });
try {
const character = await characterControllerCreate(data) as unknown as Character;
set((state) => ({
characters: [character, ...state.characters],
isLoading: false,
}));
return character;
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to create character',
isLoading: false,
});
throw error;
}
},
updateCharacter: async (id, data) => {
set({ isLoading: true, error: null });
try {
const character = await characterControllerUpdate(id, data) as unknown as Character;
set((state) => ({
characters: state.characters.map((c) => (c.id === id ? character : c)),
currentCharacter: state.currentCharacter?.id === id ? character : state.currentCharacter,
isLoading: false,
}));
return character;
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to update character',
isLoading: false,
});
throw error;
}
},
deleteCharacter: async (id) => {
set({ isLoading: true, error: null });
try {
await characterControllerDelete(id);
set((state) => ({
characters: state.characters.filter((c) => c.id !== id),
currentCharacter: state.currentCharacter?.id === id ? null : state.currentCharacter,
isLoading: false,
}));
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to delete character',
isLoading: false,
});
throw error;
}
},
setCurrentCharacter: (character) => set({ currentCharacter: character }),
clearError: () => set({ error: null }),
}));

View File

@@ -0,0 +1,144 @@
import { create } from 'zustand';
import {
chatControllerCreateConversation,
chatControllerGetConversations,
chatControllerGetConversation,
chatControllerDeleteConversation,
chatControllerSendMessage,
} from '../api/generated/conversations/conversations';
import type { CreateConversationDto, SendMessageDto } from '../api/generated/model';
import type { Conversation, ConversationWithMessages, Message } from '../types';
interface ChatState {
conversations: Conversation[];
currentConversation: ConversationWithMessages | null;
isLoading: boolean;
isStreaming: boolean;
error: string | null;
// Actions
fetchConversations: () => Promise<void>;
getConversation: (id: string) => Promise<void>;
createConversation: (data: CreateConversationDto) => Promise<Conversation>;
deleteConversation: (id: string) => Promise<void>;
sendMessage: (conversationId: string, data: SendMessageDto) => Promise<void>;
addMessage: (message: Message) => void;
setCurrentConversation: (conversation: ConversationWithMessages | null) => void;
setStreaming: (isStreaming: boolean) => void;
clearError: () => void;
}
export const useChatStore = create<ChatState>()((set) => ({
conversations: [],
currentConversation: null,
isLoading: false,
isStreaming: false,
error: null,
fetchConversations: async () => {
set({ isLoading: true, error: null });
try {
const conversations = await chatControllerGetConversations() as unknown as Conversation[];
set({ conversations, isLoading: false });
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to fetch conversations',
isLoading: false,
});
}
},
getConversation: async (id) => {
set({ isLoading: true, error: null });
try {
const conversation = await chatControllerGetConversation(id) as unknown as ConversationWithMessages;
set({ currentConversation: conversation, isLoading: false });
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to fetch conversation',
isLoading: false,
});
}
},
createConversation: async (data) => {
set({ isLoading: true, error: null });
try {
const conversation = await chatControllerCreateConversation(data) as unknown as Conversation;
set((state) => ({
conversations: [conversation, ...state.conversations],
isLoading: false,
}));
return conversation;
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to create conversation',
isLoading: false,
});
throw error;
}
},
deleteConversation: async (id) => {
set({ isLoading: true, error: null });
try {
await chatControllerDeleteConversation(id);
set((state) => ({
conversations: state.conversations.filter((c) => c.id !== id),
currentConversation: state.currentConversation?.id === id ? null : state.currentConversation,
isLoading: false,
}));
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to delete conversation',
isLoading: false,
});
}
},
sendMessage: async (conversationId, data) => {
set({ isLoading: true, error: null });
try {
const result = await chatControllerSendMessage(conversationId, data);
set((state) => {
if (!state.currentConversation) return state;
const newMessages: Message[] = [
result.userMessage as Message,
result.assistantMessage as Message,
];
return {
currentConversation: {
...state.currentConversation,
messages: [...state.currentConversation.messages, ...newMessages],
},
isLoading: false,
};
});
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to send message',
isLoading: false,
});
}
},
addMessage: (message) => {
set((state) => {
if (!state.currentConversation) return state;
return {
currentConversation: {
...state.currentConversation,
messages: [...state.currentConversation.messages, message],
},
};
});
},
setCurrentConversation: (conversation) => set({ currentConversation: conversation }),
setStreaming: (isStreaming) => set({ isStreaming }),
clearError: () => set({ error: null }),
}));