Phase 1 complete
This commit is contained in:
7
apps/frontend/openapitools.json
Normal file
7
apps/frontend/openapitools.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
|
||||
"spaces": 2,
|
||||
"generator-cli": {
|
||||
"version": "7.20.0"
|
||||
}
|
||||
}
|
||||
24
apps/frontend/orval.config.js
Normal file
24
apps/frontend/orval.config.js
Normal file
@@ -0,0 +1,24 @@
|
||||
module.exports = {
|
||||
dreamchat: {
|
||||
input: {
|
||||
target: '../openapi/openapi.json',
|
||||
},
|
||||
output: {
|
||||
mode: 'tags-split',
|
||||
target: './src/api/generated',
|
||||
schemas: './src/api/generated/model',
|
||||
client: 'fetch',
|
||||
baseUrl: 'http://localhost:3000',
|
||||
mock: false,
|
||||
override: {
|
||||
fetch: {
|
||||
includeHttpResponseReturnType: false,
|
||||
},
|
||||
mutator: {
|
||||
path: './src/api/mutator/custom-fetch.ts',
|
||||
name: 'customFetch',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -8,7 +8,8 @@
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"lint": "eslint . --ext ts,tsx",
|
||||
"clean": "rm -rf dist"
|
||||
"clean": "rm -rf dist",
|
||||
"api:generate": "orval"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dreamchat/shared": "workspace:*",
|
||||
@@ -20,10 +21,13 @@
|
||||
"zustand": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openapitools/openapi-generator-cli": "^2.29.0",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"autoprefixer": "^10.4.0",
|
||||
"openapi-typescript": "^7.13.0",
|
||||
"orval": "^8.4.2",
|
||||
"postcss": "^8.4.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5.3.0",
|
||||
|
||||
@@ -1,13 +1,132 @@
|
||||
import { useEffect } from 'react';
|
||||
import { BrowserRouter, Routes, Route, Navigate, useSearchParams, useNavigate } from 'react-router-dom';
|
||||
import { useAuthStore } from './stores/authStore';
|
||||
import { Login } from './pages/Login';
|
||||
import { CharacterList } from './pages/CharacterList';
|
||||
import { CharacterForm } from './pages/CharacterForm';
|
||||
import { ConversationList } from './pages/ConversationList';
|
||||
import { Chat } from './pages/Chat';
|
||||
|
||||
// OAuth Callback Handler - processes tokens from URL before routing
|
||||
function OAuthCallbackHandler({ children }: { children: React.ReactNode }) {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const { setTokens } = useAuthStore();
|
||||
|
||||
useEffect(() => {
|
||||
const accessToken = searchParams.get('accessToken');
|
||||
const refreshToken = searchParams.get('refreshToken');
|
||||
const errorMsg = searchParams.get('error');
|
||||
|
||||
if (errorMsg) {
|
||||
// Redirect to login with error
|
||||
const decodedError = decodeURIComponent(errorMsg);
|
||||
setSearchParams({}, { replace: true });
|
||||
navigate(`/login?error=${encodeURIComponent(decodedError)}`, { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (accessToken && refreshToken) {
|
||||
// Store tokens
|
||||
setTokens(accessToken, refreshToken);
|
||||
// Clear tokens from URL but keep the path
|
||||
setSearchParams({}, { replace: true });
|
||||
}
|
||||
}, [searchParams, setSearchParams, navigate, setTokens]);
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
function PrivateRoute({ children }: { children: React.ReactNode }) {
|
||||
const { isAuthenticated } = useAuthStore();
|
||||
|
||||
return isAuthenticated ? <>{children}</> : <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
function PublicRoute({ children }: { children: React.ReactNode }) {
|
||||
const { isAuthenticated } = useAuthStore();
|
||||
|
||||
return !isAuthenticated ? <>{children}</> : <Navigate to="/characters" replace />;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const { init } = useAuthStore();
|
||||
|
||||
useEffect(() => {
|
||||
init();
|
||||
}, [init]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold text-primary mb-4">DreamChat</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Character simulation and interactive storytelling platform
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<BrowserRouter>
|
||||
<OAuthCallbackHandler>
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/characters" replace />} />
|
||||
|
||||
<Route
|
||||
path="/login"
|
||||
element={
|
||||
<PublicRoute>
|
||||
<Login />
|
||||
</PublicRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/characters"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<CharacterList />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/characters/new"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<CharacterForm />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/characters/:id"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<CharacterForm />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/conversations"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<ConversationList />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/conversations/:conversationId"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Chat />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/chat/:characterId"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Chat />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</OAuthCallbackHandler>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
173
apps/frontend/src/api/generated/auth/auth.ts
Normal file
173
apps/frontend/src/api/generated/auth/auth.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import type {
|
||||
AuthControllerKeycloakCallbackParams,
|
||||
AuthControllerKeycloakLoginParams,
|
||||
AuthResponseDto,
|
||||
KeycloakConfigDto,
|
||||
KeycloakLoginUrlDto,
|
||||
LoginDto,
|
||||
RefreshTokenDto
|
||||
} from '.././model';
|
||||
|
||||
import { customFetch } from '../../mutator/custom-fetch';
|
||||
|
||||
/**
|
||||
* @summary Login with email and password
|
||||
*/
|
||||
export const getAuthControllerLoginUrl = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `http://localhost:3000/api/auth/login`
|
||||
}
|
||||
|
||||
export const authControllerLogin = async (loginDto: LoginDto, options?: RequestInit): Promise<AuthResponseDto> => {
|
||||
|
||||
return customFetch<AuthResponseDto>(getAuthControllerLoginUrl(),
|
||||
{
|
||||
...options,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||
body: JSON.stringify(
|
||||
loginDto,)
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
/**
|
||||
* @summary Refresh access token
|
||||
*/
|
||||
export const getAuthControllerRefreshTokensUrl = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `http://localhost:3000/api/auth/refresh`
|
||||
}
|
||||
|
||||
export const authControllerRefreshTokens = async (refreshTokenDto: RefreshTokenDto, options?: RequestInit): Promise<AuthResponseDto> => {
|
||||
|
||||
return customFetch<AuthResponseDto>(getAuthControllerRefreshTokensUrl(),
|
||||
{
|
||||
...options,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||
body: JSON.stringify(
|
||||
refreshTokenDto,)
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
/**
|
||||
* @summary Get Keycloak configuration for frontend
|
||||
*/
|
||||
export const getAuthControllerGetKeycloakConfigUrl = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `http://localhost:3000/api/auth/keycloak/config`
|
||||
}
|
||||
|
||||
export const authControllerGetKeycloakConfig = async ( options?: RequestInit): Promise<KeycloakConfigDto> => {
|
||||
|
||||
return customFetch<KeycloakConfigDto>(getAuthControllerGetKeycloakConfigUrl(),
|
||||
{
|
||||
...options,
|
||||
method: 'GET'
|
||||
|
||||
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
/**
|
||||
* @summary Get Keycloak login URL (initiates OAuth flow)
|
||||
*/
|
||||
export const getAuthControllerKeycloakLoginUrl = (params?: AuthControllerKeycloakLoginParams,) => {
|
||||
const normalizedParams = new URLSearchParams();
|
||||
|
||||
Object.entries(params || {}).forEach(([key, value]) => {
|
||||
|
||||
if (value !== undefined) {
|
||||
normalizedParams.append(key, value === null ? 'null' : value.toString())
|
||||
}
|
||||
});
|
||||
|
||||
const stringifiedParams = normalizedParams.toString();
|
||||
|
||||
return stringifiedParams.length > 0 ? `http://localhost:3000/api/auth/keycloak/login?${stringifiedParams}` : `http://localhost:3000/api/auth/keycloak/login`
|
||||
}
|
||||
|
||||
export const authControllerKeycloakLogin = async (params?: AuthControllerKeycloakLoginParams, options?: RequestInit): Promise<KeycloakLoginUrlDto> => {
|
||||
|
||||
return customFetch<KeycloakLoginUrlDto>(getAuthControllerKeycloakLoginUrl(params),
|
||||
{
|
||||
...options,
|
||||
method: 'GET'
|
||||
|
||||
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
/**
|
||||
* @summary Keycloak OAuth callback endpoint
|
||||
*/
|
||||
export const getAuthControllerKeycloakCallbackUrl = (params: AuthControllerKeycloakCallbackParams,) => {
|
||||
const normalizedParams = new URLSearchParams();
|
||||
|
||||
Object.entries(params || {}).forEach(([key, value]) => {
|
||||
|
||||
if (value !== undefined) {
|
||||
normalizedParams.append(key, value === null ? 'null' : value.toString())
|
||||
}
|
||||
});
|
||||
|
||||
const stringifiedParams = normalizedParams.toString();
|
||||
|
||||
return stringifiedParams.length > 0 ? `http://localhost:3000/api/auth/keycloak/callback?${stringifiedParams}` : `http://localhost:3000/api/auth/keycloak/callback`
|
||||
}
|
||||
|
||||
export const authControllerKeycloakCallback = async (params: AuthControllerKeycloakCallbackParams, options?: RequestInit): Promise<unknown> => {
|
||||
|
||||
return customFetch<unknown>(getAuthControllerKeycloakCallbackUrl(params),
|
||||
{
|
||||
...options,
|
||||
method: 'GET'
|
||||
|
||||
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
/**
|
||||
* @summary Login with Keycloak bearer token (Authorization: Bearer <keycloak-jwt>)
|
||||
*/
|
||||
export const getAuthControllerKeycloakBearerLoginUrl = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `http://localhost:3000/api/auth/keycloak`
|
||||
}
|
||||
|
||||
export const authControllerKeycloakBearerLogin = async ( options?: RequestInit): Promise<AuthResponseDto> => {
|
||||
|
||||
return customFetch<AuthResponseDto>(getAuthControllerKeycloakBearerLoginUrl(),
|
||||
{
|
||||
...options,
|
||||
method: 'POST'
|
||||
|
||||
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
133
apps/frontend/src/api/generated/characters/characters.ts
Normal file
133
apps/frontend/src/api/generated/characters/characters.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import type {
|
||||
CharacterResponseDto,
|
||||
CreateCharacterDto,
|
||||
UpdateCharacterDto
|
||||
} from '.././model';
|
||||
|
||||
import { customFetch } from '../../mutator/custom-fetch';
|
||||
|
||||
/**
|
||||
* @summary Create a new character
|
||||
*/
|
||||
export const getCharacterControllerCreateUrl = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `http://localhost:3000/api/characters`
|
||||
}
|
||||
|
||||
export const characterControllerCreate = async (createCharacterDto: CreateCharacterDto, options?: RequestInit): Promise<CharacterResponseDto> => {
|
||||
|
||||
return customFetch<CharacterResponseDto>(getCharacterControllerCreateUrl(),
|
||||
{
|
||||
...options,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||
body: JSON.stringify(
|
||||
createCharacterDto,)
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
/**
|
||||
* @summary Get all characters for current user
|
||||
*/
|
||||
export const getCharacterControllerFindAllUrl = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `http://localhost:3000/api/characters`
|
||||
}
|
||||
|
||||
export const characterControllerFindAll = async ( options?: RequestInit): Promise<CharacterResponseDto[]> => {
|
||||
|
||||
return customFetch<CharacterResponseDto[]>(getCharacterControllerFindAllUrl(),
|
||||
{
|
||||
...options,
|
||||
method: 'GET'
|
||||
|
||||
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
/**
|
||||
* @summary Get character by ID
|
||||
*/
|
||||
export const getCharacterControllerFindOneUrl = (id: string,) => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `http://localhost:3000/api/characters/${id}`
|
||||
}
|
||||
|
||||
export const characterControllerFindOne = async (id: string, options?: RequestInit): Promise<CharacterResponseDto> => {
|
||||
|
||||
return customFetch<CharacterResponseDto>(getCharacterControllerFindOneUrl(id),
|
||||
{
|
||||
...options,
|
||||
method: 'GET'
|
||||
|
||||
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
/**
|
||||
* @summary Update character
|
||||
*/
|
||||
export const getCharacterControllerUpdateUrl = (id: string,) => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `http://localhost:3000/api/characters/${id}`
|
||||
}
|
||||
|
||||
export const characterControllerUpdate = async (id: string,
|
||||
updateCharacterDto: UpdateCharacterDto, options?: RequestInit): Promise<CharacterResponseDto> => {
|
||||
|
||||
return customFetch<CharacterResponseDto>(getCharacterControllerUpdateUrl(id),
|
||||
{
|
||||
...options,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||
body: JSON.stringify(
|
||||
updateCharacterDto,)
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
/**
|
||||
* @summary Delete character
|
||||
*/
|
||||
export const getCharacterControllerDeleteUrl = (id: string,) => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `http://localhost:3000/api/characters/${id}`
|
||||
}
|
||||
|
||||
export const characterControllerDelete = async (id: string, options?: RequestInit): Promise<void> => {
|
||||
|
||||
return customFetch<void>(getCharacterControllerDeleteUrl(id),
|
||||
{
|
||||
...options,
|
||||
method: 'DELETE'
|
||||
|
||||
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
135
apps/frontend/src/api/generated/conversations/conversations.ts
Normal file
135
apps/frontend/src/api/generated/conversations/conversations.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import type {
|
||||
ConversationResponseDto,
|
||||
ConversationWithMessagesResponseDto,
|
||||
CreateConversationDto,
|
||||
SendMessageDto,
|
||||
SendMessageResponseDto
|
||||
} from '.././model';
|
||||
|
||||
import { customFetch } from '../../mutator/custom-fetch';
|
||||
|
||||
/**
|
||||
* @summary Create a new conversation
|
||||
*/
|
||||
export const getChatControllerCreateConversationUrl = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `http://localhost:3000/api/conversations`
|
||||
}
|
||||
|
||||
export const chatControllerCreateConversation = async (createConversationDto: CreateConversationDto, options?: RequestInit): Promise<ConversationResponseDto> => {
|
||||
|
||||
return customFetch<ConversationResponseDto>(getChatControllerCreateConversationUrl(),
|
||||
{
|
||||
...options,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||
body: JSON.stringify(
|
||||
createConversationDto,)
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
/**
|
||||
* @summary Get all conversations for current user
|
||||
*/
|
||||
export const getChatControllerGetConversationsUrl = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `http://localhost:3000/api/conversations`
|
||||
}
|
||||
|
||||
export const chatControllerGetConversations = async ( options?: RequestInit): Promise<ConversationResponseDto[]> => {
|
||||
|
||||
return customFetch<ConversationResponseDto[]>(getChatControllerGetConversationsUrl(),
|
||||
{
|
||||
...options,
|
||||
method: 'GET'
|
||||
|
||||
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
/**
|
||||
* @summary Get conversation by ID with messages
|
||||
*/
|
||||
export const getChatControllerGetConversationUrl = (id: string,) => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `http://localhost:3000/api/conversations/${id}`
|
||||
}
|
||||
|
||||
export const chatControllerGetConversation = async (id: string, options?: RequestInit): Promise<ConversationWithMessagesResponseDto> => {
|
||||
|
||||
return customFetch<ConversationWithMessagesResponseDto>(getChatControllerGetConversationUrl(id),
|
||||
{
|
||||
...options,
|
||||
method: 'GET'
|
||||
|
||||
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
/**
|
||||
* @summary Delete conversation
|
||||
*/
|
||||
export const getChatControllerDeleteConversationUrl = (id: string,) => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `http://localhost:3000/api/conversations/${id}`
|
||||
}
|
||||
|
||||
export const chatControllerDeleteConversation = async (id: string, options?: RequestInit): Promise<void> => {
|
||||
|
||||
return customFetch<void>(getChatControllerDeleteConversationUrl(id),
|
||||
{
|
||||
...options,
|
||||
method: 'DELETE'
|
||||
|
||||
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
/**
|
||||
* @summary Send a message in a conversation
|
||||
*/
|
||||
export const getChatControllerSendMessageUrl = (id: string,) => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `http://localhost:3000/api/conversations/${id}/messages`
|
||||
}
|
||||
|
||||
export const chatControllerSendMessage = async (id: string,
|
||||
sendMessageDto: SendMessageDto, options?: RequestInit): Promise<SendMessageResponseDto> => {
|
||||
|
||||
return customFetch<SendMessageResponseDto>(getChatControllerSendMessageUrl(id),
|
||||
{
|
||||
...options,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||
body: JSON.stringify(
|
||||
sendMessageDto,)
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
111
apps/frontend/src/api/generated/import/import.ts
Normal file
111
apps/frontend/src/api/generated/import/import.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import type {
|
||||
ImportControllerUploadFileBody
|
||||
} from '.././model';
|
||||
|
||||
import { customFetch } from '../../mutator/custom-fetch';
|
||||
|
||||
/**
|
||||
* @summary Upload a file for character knowledge
|
||||
*/
|
||||
export const getImportControllerUploadFileUrl = (characterId: string,) => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `http://localhost:3000/api/import/characters/${characterId}/files`
|
||||
}
|
||||
|
||||
export const importControllerUploadFile = async (characterId: string,
|
||||
importControllerUploadFileBody: ImportControllerUploadFileBody, options?: RequestInit): Promise<void> => {
|
||||
const formData = new FormData();
|
||||
if(importControllerUploadFileBody.file !== undefined) {
|
||||
formData.append(`file`, importControllerUploadFileBody.file);
|
||||
}
|
||||
|
||||
return customFetch<void>(getImportControllerUploadFileUrl(characterId),
|
||||
{
|
||||
...options,
|
||||
method: 'POST'
|
||||
,
|
||||
body:
|
||||
formData,
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
/**
|
||||
* @summary Get knowledge processing status
|
||||
*/
|
||||
export const getImportControllerGetKnowledgeStatusUrl = (knowledgeId: string,) => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `http://localhost:3000/api/import/knowledge/${knowledgeId}/status`
|
||||
}
|
||||
|
||||
export const importControllerGetKnowledgeStatus = async (knowledgeId: string, options?: RequestInit): Promise<void> => {
|
||||
|
||||
return customFetch<void>(getImportControllerGetKnowledgeStatusUrl(knowledgeId),
|
||||
{
|
||||
...options,
|
||||
method: 'GET'
|
||||
|
||||
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
/**
|
||||
* @summary Get all knowledge for a character
|
||||
*/
|
||||
export const getImportControllerGetCharacterKnowledgeUrl = (characterId: string,) => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `http://localhost:3000/api/import/characters/${characterId}/knowledge`
|
||||
}
|
||||
|
||||
export const importControllerGetCharacterKnowledge = async (characterId: string, options?: RequestInit): Promise<void> => {
|
||||
|
||||
return customFetch<void>(getImportControllerGetCharacterKnowledgeUrl(characterId),
|
||||
{
|
||||
...options,
|
||||
method: 'GET'
|
||||
|
||||
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
/**
|
||||
* @summary Delete knowledge
|
||||
*/
|
||||
export const getImportControllerDeleteKnowledgeUrl = (knowledgeId: string,) => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `http://localhost:3000/api/import/knowledge/${knowledgeId}`
|
||||
}
|
||||
|
||||
export const importControllerDeleteKnowledge = async (knowledgeId: string, options?: RequestInit): Promise<void> => {
|
||||
|
||||
return customFetch<void>(getImportControllerDeleteKnowledgeUrl(knowledgeId),
|
||||
{
|
||||
...options,
|
||||
method: 'DELETE'
|
||||
|
||||
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export type AuthControllerKeycloakCallbackParams = {
|
||||
/**
|
||||
* Authorization code from Keycloak
|
||||
*/
|
||||
code: string;
|
||||
/**
|
||||
* Error message if authentication failed
|
||||
*/
|
||||
error?: string;
|
||||
/**
|
||||
* Error description
|
||||
*/
|
||||
error_description?: string;
|
||||
/**
|
||||
* State parameter for CSRF validation
|
||||
*/
|
||||
state: string;
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export type AuthControllerKeycloakLoginParams = {
|
||||
/**
|
||||
* Frontend path to redirect after login
|
||||
*/
|
||||
redirectTo?: string;
|
||||
};
|
||||
17
apps/frontend/src/api/generated/model/authResponseDto.ts
Normal file
17
apps/frontend/src/api/generated/model/authResponseDto.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import type { UserDto } from './userDto';
|
||||
|
||||
export interface AuthResponseDto {
|
||||
/** JWT access token */
|
||||
accessToken: string;
|
||||
/** JWT refresh token */
|
||||
refreshToken: string;
|
||||
/** User information */
|
||||
user: UserDto;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import type { CharacterResponseDtoAttributes } from './characterResponseDtoAttributes';
|
||||
import type { CharacterResponseDtoConfig } from './characterResponseDtoConfig';
|
||||
|
||||
export interface CharacterResponseDto {
|
||||
/** Character ID */
|
||||
id: string;
|
||||
/** Character name */
|
||||
name: string;
|
||||
/** Avatar URL */
|
||||
avatarUrl?: string;
|
||||
/** Personality prompt */
|
||||
personalityPrompt: string;
|
||||
/** Custom attributes */
|
||||
attributes: CharacterResponseDtoAttributes;
|
||||
/** Character configuration */
|
||||
config: CharacterResponseDtoConfig;
|
||||
/** Whether character is public */
|
||||
isPublic: boolean;
|
||||
/** Creation date */
|
||||
createdAt: string;
|
||||
/** Last update date */
|
||||
updatedAt: string;
|
||||
/** User ID */
|
||||
userId: string;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Custom attributes
|
||||
*/
|
||||
export type CharacterResponseDtoAttributes = { [key: string]: unknown };
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Character configuration
|
||||
*/
|
||||
export type CharacterResponseDtoConfig = { [key: string]: unknown };
|
||||
16
apps/frontend/src/api/generated/model/characterSummaryDto.ts
Normal file
16
apps/frontend/src/api/generated/model/characterSummaryDto.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export interface CharacterSummaryDto {
|
||||
/** Character ID */
|
||||
id: string;
|
||||
/** Character name */
|
||||
name: string;
|
||||
/** Avatar URL */
|
||||
avatarUrl?: string;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import type { CharacterSummaryDto } from './characterSummaryDto';
|
||||
|
||||
export interface ConversationResponseDto {
|
||||
/** Conversation ID */
|
||||
id: string;
|
||||
/** Conversation title */
|
||||
title?: string;
|
||||
/** Character ID */
|
||||
characterId: string;
|
||||
/** Number of messages */
|
||||
messageCount: number;
|
||||
/** Total tokens used */
|
||||
totalTokens: number;
|
||||
/** Creation date */
|
||||
createdAt: string;
|
||||
/** Last update date */
|
||||
updatedAt: string;
|
||||
/** Character info */
|
||||
character?: CharacterSummaryDto;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import type { CharacterSummaryDto } from './characterSummaryDto';
|
||||
import type { MessageResponseDto } from './messageResponseDto';
|
||||
|
||||
export interface ConversationWithMessagesResponseDto {
|
||||
/** Conversation ID */
|
||||
id: string;
|
||||
/** Conversation title */
|
||||
title?: string;
|
||||
/** Character ID */
|
||||
characterId: string;
|
||||
/** Number of messages */
|
||||
messageCount: number;
|
||||
/** Total tokens used */
|
||||
totalTokens: number;
|
||||
/** Creation date */
|
||||
createdAt: string;
|
||||
/** Last update date */
|
||||
updatedAt: string;
|
||||
/** Character info */
|
||||
character?: CharacterSummaryDto;
|
||||
/** Messages in conversation */
|
||||
messages: MessageResponseDto[];
|
||||
}
|
||||
24
apps/frontend/src/api/generated/model/createCharacterDto.ts
Normal file
24
apps/frontend/src/api/generated/model/createCharacterDto.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import type { CreateCharacterDtoAttributes } from './createCharacterDtoAttributes';
|
||||
import type { CreateCharacterDtoConfig } from './createCharacterDtoConfig';
|
||||
|
||||
export interface CreateCharacterDto {
|
||||
/** Character name */
|
||||
name: string;
|
||||
/** Avatar URL */
|
||||
avatarUrl?: string;
|
||||
/** Personality prompt that guides AI responses */
|
||||
personalityPrompt: string;
|
||||
/** Custom attributes (JSON) */
|
||||
attributes?: CreateCharacterDtoAttributes;
|
||||
/** Character configuration (JSON) */
|
||||
config?: CreateCharacterDtoConfig;
|
||||
/** Whether the character is publicly visible */
|
||||
isPublic?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Custom attributes (JSON)
|
||||
*/
|
||||
export type CreateCharacterDtoAttributes = { [key: string]: unknown };
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Character configuration (JSON)
|
||||
*/
|
||||
export type CreateCharacterDtoConfig = { [key: string]: unknown };
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export interface CreateConversationDto {
|
||||
/** Character ID to chat with */
|
||||
characterId: string;
|
||||
/** Conversation title */
|
||||
title?: string;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export type ImportControllerUploadFileBody = {
|
||||
/** File to upload (.txt, .md) */
|
||||
file?: Blob;
|
||||
};
|
||||
37
apps/frontend/src/api/generated/model/index.ts
Normal file
37
apps/frontend/src/api/generated/model/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export * from './authControllerKeycloakCallbackParams';
|
||||
export * from './authControllerKeycloakLoginParams';
|
||||
export * from './authResponseDto';
|
||||
export * from './characterResponseDto';
|
||||
export * from './characterResponseDtoAttributes';
|
||||
export * from './characterResponseDtoConfig';
|
||||
export * from './characterSummaryDto';
|
||||
export * from './conversationResponseDto';
|
||||
export * from './conversationWithMessagesResponseDto';
|
||||
export * from './createCharacterDto';
|
||||
export * from './createCharacterDtoAttributes';
|
||||
export * from './createCharacterDtoConfig';
|
||||
export * from './createConversationDto';
|
||||
export * from './importControllerUploadFileBody';
|
||||
export * from './keycloakConfigDto';
|
||||
export * from './keycloakLoginUrlDto';
|
||||
export * from './loginDto';
|
||||
export * from './messageResponseDto';
|
||||
export * from './messageResponseDtoRole';
|
||||
export * from './refreshTokenDto';
|
||||
export * from './sendMessageDto';
|
||||
export * from './sendMessageResponseDto';
|
||||
export * from './updateCharacterDto';
|
||||
export * from './updateCharacterDtoAttributes';
|
||||
export * from './updateCharacterDtoConfig';
|
||||
export * from './updatePasswordDto';
|
||||
export * from './updateUserDto';
|
||||
export * from './userDto';
|
||||
export * from './userDtoRole';
|
||||
18
apps/frontend/src/api/generated/model/keycloakConfigDto.ts
Normal file
18
apps/frontend/src/api/generated/model/keycloakConfigDto.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export interface KeycloakConfigDto {
|
||||
/** Whether Keycloak authentication is enabled */
|
||||
enabled: boolean;
|
||||
/** Keycloak realm URL */
|
||||
url?: string;
|
||||
/** Keycloak realm name */
|
||||
realm?: string;
|
||||
/** Keycloak client ID */
|
||||
clientId?: string;
|
||||
}
|
||||
14
apps/frontend/src/api/generated/model/keycloakLoginUrlDto.ts
Normal file
14
apps/frontend/src/api/generated/model/keycloakLoginUrlDto.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export interface KeycloakLoginUrlDto {
|
||||
/** Keycloak login URL to redirect the user to */
|
||||
loginUrl: string;
|
||||
/** State parameter for CSRF protection */
|
||||
state: string;
|
||||
}
|
||||
14
apps/frontend/src/api/generated/model/loginDto.ts
Normal file
14
apps/frontend/src/api/generated/model/loginDto.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export interface LoginDto {
|
||||
/** User email address */
|
||||
email: string;
|
||||
/** User password */
|
||||
password: string;
|
||||
}
|
||||
23
apps/frontend/src/api/generated/model/messageResponseDto.ts
Normal file
23
apps/frontend/src/api/generated/model/messageResponseDto.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import type { MessageResponseDtoRole } from './messageResponseDtoRole';
|
||||
|
||||
export interface MessageResponseDto {
|
||||
/** Message ID */
|
||||
id: string;
|
||||
/** Message role */
|
||||
role: MessageResponseDtoRole;
|
||||
/** Message content */
|
||||
content: string;
|
||||
/** Tokens used */
|
||||
tokensUsed?: number;
|
||||
/** Model used */
|
||||
model?: string;
|
||||
/** Creation date */
|
||||
createdAt: string;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Message role
|
||||
*/
|
||||
export type MessageResponseDtoRole = typeof MessageResponseDtoRole[keyof typeof MessageResponseDtoRole];
|
||||
|
||||
|
||||
export const MessageResponseDtoRole = {
|
||||
user: 'user',
|
||||
assistant: 'assistant',
|
||||
system: 'system',
|
||||
} as const;
|
||||
12
apps/frontend/src/api/generated/model/refreshTokenDto.ts
Normal file
12
apps/frontend/src/api/generated/model/refreshTokenDto.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export interface RefreshTokenDto {
|
||||
/** Refresh token */
|
||||
refreshToken: string;
|
||||
}
|
||||
12
apps/frontend/src/api/generated/model/sendMessageDto.ts
Normal file
12
apps/frontend/src/api/generated/model/sendMessageDto.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export interface SendMessageDto {
|
||||
/** Message content */
|
||||
content: string;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import type { MessageResponseDto } from './messageResponseDto';
|
||||
|
||||
export interface SendMessageResponseDto {
|
||||
/** User message */
|
||||
userMessage: MessageResponseDto;
|
||||
/** Assistant response */
|
||||
assistantMessage: MessageResponseDto;
|
||||
}
|
||||
24
apps/frontend/src/api/generated/model/updateCharacterDto.ts
Normal file
24
apps/frontend/src/api/generated/model/updateCharacterDto.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import type { UpdateCharacterDtoAttributes } from './updateCharacterDtoAttributes';
|
||||
import type { UpdateCharacterDtoConfig } from './updateCharacterDtoConfig';
|
||||
|
||||
export interface UpdateCharacterDto {
|
||||
/** Character name */
|
||||
name?: string;
|
||||
/** Avatar URL */
|
||||
avatarUrl?: string;
|
||||
/** Personality prompt */
|
||||
personalityPrompt?: string;
|
||||
/** Custom attributes (JSON) */
|
||||
attributes?: UpdateCharacterDtoAttributes;
|
||||
/** Character configuration (JSON) */
|
||||
config?: UpdateCharacterDtoConfig;
|
||||
/** Whether the character is publicly visible */
|
||||
isPublic?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Custom attributes (JSON)
|
||||
*/
|
||||
export type UpdateCharacterDtoAttributes = { [key: string]: unknown };
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Character configuration (JSON)
|
||||
*/
|
||||
export type UpdateCharacterDtoConfig = { [key: string]: unknown };
|
||||
14
apps/frontend/src/api/generated/model/updatePasswordDto.ts
Normal file
14
apps/frontend/src/api/generated/model/updatePasswordDto.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export interface UpdatePasswordDto {
|
||||
/** Current password */
|
||||
currentPassword?: string;
|
||||
/** New password */
|
||||
newPassword?: string;
|
||||
}
|
||||
14
apps/frontend/src/api/generated/model/updateUserDto.ts
Normal file
14
apps/frontend/src/api/generated/model/updateUserDto.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
export interface UpdateUserDto {
|
||||
/** New email address */
|
||||
email?: string;
|
||||
/** New username */
|
||||
username?: string;
|
||||
}
|
||||
19
apps/frontend/src/api/generated/model/userDto.ts
Normal file
19
apps/frontend/src/api/generated/model/userDto.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import type { UserDtoRole } from './userDtoRole';
|
||||
|
||||
export interface UserDto {
|
||||
/** User ID */
|
||||
id: string;
|
||||
/** User email */
|
||||
email: string;
|
||||
/** User username */
|
||||
username: string;
|
||||
/** User role */
|
||||
role: UserDtoRole;
|
||||
}
|
||||
18
apps/frontend/src/api/generated/model/userDtoRole.ts
Normal file
18
apps/frontend/src/api/generated/model/userDtoRole.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* User role
|
||||
*/
|
||||
export type UserDtoRole = typeof UserDtoRole[keyof typeof UserDtoRole];
|
||||
|
||||
|
||||
export const UserDtoRole = {
|
||||
USER: 'USER',
|
||||
ADMIN: 'ADMIN',
|
||||
} as const;
|
||||
108
apps/frontend/src/api/generated/users/users.ts
Normal file
108
apps/frontend/src/api/generated/users/users.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Generated by orval v8.4.2 🍺
|
||||
* Do not edit manually.
|
||||
* DreamChat API
|
||||
* The DreamChat API documentation
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*/
|
||||
import type {
|
||||
UpdatePasswordDto,
|
||||
UpdateUserDto
|
||||
} from '.././model';
|
||||
|
||||
import { customFetch } from '../../mutator/custom-fetch';
|
||||
|
||||
/**
|
||||
* @summary Get current user profile
|
||||
*/
|
||||
export const getUserControllerGetProfileUrl = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `http://localhost:3000/api/users/me`
|
||||
}
|
||||
|
||||
export const userControllerGetProfile = async ( options?: RequestInit): Promise<void> => {
|
||||
|
||||
return customFetch<void>(getUserControllerGetProfileUrl(),
|
||||
{
|
||||
...options,
|
||||
method: 'GET'
|
||||
|
||||
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
/**
|
||||
* @summary Update current user profile
|
||||
*/
|
||||
export const getUserControllerUpdateProfileUrl = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `http://localhost:3000/api/users/me`
|
||||
}
|
||||
|
||||
export const userControllerUpdateProfile = async (updateUserDto: UpdateUserDto, options?: RequestInit): Promise<void> => {
|
||||
|
||||
return customFetch<void>(getUserControllerUpdateProfileUrl(),
|
||||
{
|
||||
...options,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||
body: JSON.stringify(
|
||||
updateUserDto,)
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
/**
|
||||
* @summary Delete user account
|
||||
*/
|
||||
export const getUserControllerDeleteAccountUrl = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `http://localhost:3000/api/users/me`
|
||||
}
|
||||
|
||||
export const userControllerDeleteAccount = async ( options?: RequestInit): Promise<void> => {
|
||||
|
||||
return customFetch<void>(getUserControllerDeleteAccountUrl(),
|
||||
{
|
||||
...options,
|
||||
method: 'DELETE'
|
||||
|
||||
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
/**
|
||||
* @summary Update user password
|
||||
*/
|
||||
export const getUserControllerUpdatePasswordUrl = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
return `http://localhost:3000/api/users/me/password`
|
||||
}
|
||||
|
||||
export const userControllerUpdatePassword = async (updatePasswordDto: UpdatePasswordDto, options?: RequestInit): Promise<void> => {
|
||||
|
||||
return customFetch<void>(getUserControllerUpdatePasswordUrl(),
|
||||
{
|
||||
...options,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||
body: JSON.stringify(
|
||||
updatePasswordDto,)
|
||||
}
|
||||
);}
|
||||
|
||||
|
||||
44
apps/frontend/src/api/mutator/custom-fetch.ts
Normal file
44
apps/frontend/src/api/mutator/custom-fetch.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
const getBaseUrl = () => {
|
||||
return (import.meta.env as unknown as ImportMetaEnv).VITE_API_URL || 'http://localhost:3000/api';
|
||||
};
|
||||
|
||||
const getToken = (): string | null => {
|
||||
return localStorage.getItem('accessToken');
|
||||
};
|
||||
|
||||
export const customFetch = async <T>(
|
||||
url: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> => {
|
||||
// If URL is already absolute (starts with http), use it as-is
|
||||
// Otherwise, prepend the base URL
|
||||
const fullUrl = url.startsWith('http') ? url : `${getBaseUrl()}${url}`;
|
||||
|
||||
const token = getToken();
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers as Record<string, string> || {}),
|
||||
};
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const response = await fetch(fullUrl, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ message: 'Unknown error' }));
|
||||
throw new Error(error.message || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
// Handle empty responses
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
197
apps/frontend/src/pages/CharacterForm.tsx
Normal file
197
apps/frontend/src/pages/CharacterForm.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams, Link } from 'react-router-dom';
|
||||
import { useCharacterStore } from '../stores/characterStore';
|
||||
|
||||
export function CharacterForm() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const isEditing = id !== 'new';
|
||||
|
||||
const {
|
||||
currentCharacter,
|
||||
isLoading,
|
||||
error,
|
||||
getCharacter,
|
||||
createCharacter,
|
||||
updateCharacter,
|
||||
clearError,
|
||||
setCurrentCharacter
|
||||
} = useCharacterStore();
|
||||
|
||||
const [name, setName] = useState('');
|
||||
const [personalityPrompt, setPersonalityPrompt] = useState('');
|
||||
const [avatarUrl, setAvatarUrl] = useState('');
|
||||
const [isPublic, setIsPublic] = useState(false);
|
||||
const [attributes, setAttributes] = useState('{}');
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing && id) {
|
||||
getCharacter(id);
|
||||
}
|
||||
return () => {
|
||||
setCurrentCharacter(null);
|
||||
};
|
||||
}, [isEditing, id, getCharacter, setCurrentCharacter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentCharacter && isEditing) {
|
||||
setName(currentCharacter.name);
|
||||
setPersonalityPrompt(currentCharacter.personalityPrompt);
|
||||
setAvatarUrl(currentCharacter.avatarUrl || '');
|
||||
setIsPublic(currentCharacter.isPublic);
|
||||
setAttributes(JSON.stringify(currentCharacter.attributes, null, 2));
|
||||
}
|
||||
}, [currentCharacter, isEditing]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
clearError();
|
||||
|
||||
let parsedAttributes = {};
|
||||
try {
|
||||
parsedAttributes = JSON.parse(attributes);
|
||||
} catch {
|
||||
alert('Invalid JSON in attributes field');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
name,
|
||||
personalityPrompt,
|
||||
avatarUrl: avatarUrl || undefined,
|
||||
isPublic,
|
||||
attributes: parsedAttributes,
|
||||
};
|
||||
|
||||
try {
|
||||
if (isEditing && id) {
|
||||
await updateCharacter(id, data);
|
||||
} else {
|
||||
await createCharacter(data);
|
||||
}
|
||||
navigate('/characters');
|
||||
} catch {
|
||||
// Error is handled by the store
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<header className="bg-white shadow">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<Link to="/characters" className="text-gray-600 hover:text-gray-900">
|
||||
← Back to Characters
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-6">
|
||||
{isEditing ? 'Edit Character' : 'Create New Character'}
|
||||
</h1>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 bg-red-50 text-red-700 p-3 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||||
Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
required
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
||||
placeholder="e.g., Alice the Explorer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="avatarUrl" className="block text-sm font-medium text-gray-700">
|
||||
Avatar URL
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
id="avatarUrl"
|
||||
value={avatarUrl}
|
||||
onChange={(e) => setAvatarUrl(e.target.value)}
|
||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
||||
placeholder="https://example.com/avatar.jpg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="personalityPrompt" className="block text-sm font-medium text-gray-700">
|
||||
Personality Prompt *
|
||||
</label>
|
||||
<textarea
|
||||
id="personalityPrompt"
|
||||
required
|
||||
rows={6}
|
||||
value={personalityPrompt}
|
||||
onChange={(e) => setPersonalityPrompt(e.target.value)}
|
||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
||||
placeholder="Describe your character's personality, background, and how they should respond..."
|
||||
/>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
This prompt guides how the AI will respond as this character.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="attributes" className="block text-sm font-medium text-gray-700">
|
||||
Attributes (JSON)
|
||||
</label>
|
||||
<textarea
|
||||
id="attributes"
|
||||
rows={4}
|
||||
value={attributes}
|
||||
onChange={(e) => setAttributes(e.target.value)}
|
||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 font-mono text-sm"
|
||||
/>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Custom attributes for your character (JSON format)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="isPublic"
|
||||
checked={isPublic}
|
||||
onChange={(e) => setIsPublic(e.target.checked)}
|
||||
className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="isPublic" className="ml-2 block text-sm text-gray-900">
|
||||
Make this character public
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="flex-1 flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? 'Saving...' : (isEditing ? 'Update Character' : 'Create Character')}
|
||||
</button>
|
||||
<Link
|
||||
to="/characters"
|
||||
className="flex-1 flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
Cancel
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
134
apps/frontend/src/pages/CharacterList.tsx
Normal file
134
apps/frontend/src/pages/CharacterList.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useCharacterStore } from '../stores/characterStore';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
|
||||
export function CharacterList() {
|
||||
const navigate = useNavigate();
|
||||
const { logout } = useAuthStore();
|
||||
const { characters, isLoading, error, fetchCharacters, deleteCharacter, clearError } = useCharacterStore();
|
||||
|
||||
useEffect(() => {
|
||||
fetchCharacters();
|
||||
}, [fetchCharacters]);
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('Are you sure you want to delete this character?')) return;
|
||||
try {
|
||||
await deleteCharacter(id);
|
||||
} catch {
|
||||
// Error is handled by the store
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<header className="bg-white shadow">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold text-gray-900">DreamChat</h1>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link
|
||||
to="/conversations"
|
||||
className="text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
Conversations
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="text-sm text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl font-semibold text-gray-900">My Characters</h2>
|
||||
<Link
|
||||
to="/characters/new"
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700"
|
||||
>
|
||||
Create Character
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 bg-red-50 text-red-700 p-3 rounded">
|
||||
{error}
|
||||
<button onClick={clearError} className="ml-2 text-sm underline">Dismiss</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8">Loading...</div>
|
||||
) : characters.length === 0 ? (
|
||||
<div className="text-center py-12 bg-white rounded-lg shadow">
|
||||
<p className="text-gray-500">No characters yet. Create your first character to get started!</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{characters.map((character) => (
|
||||
<div key={character.id} className="bg-white rounded-lg shadow overflow-hidden">
|
||||
<div className="p-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
{character.avatarUrl ? (
|
||||
<img
|
||||
src={character.avatarUrl}
|
||||
alt={character.name}
|
||||
className="h-12 w-12 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-12 w-12 rounded-full bg-indigo-100 flex items-center justify-center">
|
||||
<span className="text-indigo-600 font-medium text-lg">
|
||||
{character.name.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900">{character.name}</h3>
|
||||
<span className="text-sm text-gray-500">
|
||||
{character.isPublic ? 'Public' : 'Private'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mt-4 text-sm text-gray-600 line-clamp-3">
|
||||
{character.personalityPrompt}
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex space-x-3">
|
||||
<Link
|
||||
to={`/characters/${character.id}`}
|
||||
className="flex-1 text-center px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
Edit
|
||||
</Link>
|
||||
<Link
|
||||
to={`/chat/${character.id}`}
|
||||
className="flex-1 text-center px-4 py-2 border border-transparent rounded-md text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700"
|
||||
>
|
||||
Chat
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => handleDelete(character.id)}
|
||||
className="px-4 py-2 border border-red-300 rounded-md text-sm font-medium text-red-700 hover:bg-red-50"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
230
apps/frontend/src/pages/Chat.tsx
Normal file
230
apps/frontend/src/pages/Chat.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
import { useChatStore } from '../stores/chatStore';
|
||||
import { useCharacterStore } from '../stores/characterStore';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import type { Message } from '../types';
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
|
||||
const WS_URL = (import.meta.env as unknown as ImportMetaEnv).VITE_WS_URL || 'http://localhost:3000';
|
||||
|
||||
export function Chat() {
|
||||
const { characterId, conversationId } = useParams<{ characterId?: string; conversationId?: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { logout } = useAuthStore();
|
||||
const { currentCharacter, getCharacter } = useCharacterStore();
|
||||
const {
|
||||
currentConversation,
|
||||
isStreaming,
|
||||
error,
|
||||
createConversation,
|
||||
getConversation,
|
||||
addMessage,
|
||||
setStreaming,
|
||||
clearError
|
||||
} = useChatStore();
|
||||
|
||||
const [message, setMessage] = useState('');
|
||||
const [streamingContent, setStreamingContent] = useState('');
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const socketRef = useRef<Socket | null>(null);
|
||||
|
||||
// Initialize socket connection
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
if (!token) return;
|
||||
|
||||
const socket = io(`${WS_URL}/chat`, {
|
||||
auth: { token: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
socketRef.current = socket;
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('Connected to chat server');
|
||||
});
|
||||
|
||||
socket.on('message_chunk', (data: { conversationId: string; chunk: { content: string } }) => {
|
||||
if (data.conversationId === conversationId) {
|
||||
setStreamingContent((prev) => prev + data.chunk.content);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('message_complete', () => {
|
||||
setStreaming(false);
|
||||
setStreamingContent('');
|
||||
if (conversationId) {
|
||||
getConversation(conversationId);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('message', (data: { message: { assistantMessage?: Message } }) => {
|
||||
if (data.message.assistantMessage) {
|
||||
addMessage(data.message.assistantMessage);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (data: { message: string }) => {
|
||||
console.error('Socket error:', data.message);
|
||||
setStreaming(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.disconnect();
|
||||
};
|
||||
}, [conversationId, addMessage, getConversation, setStreaming]);
|
||||
|
||||
// Join conversation room
|
||||
useEffect(() => {
|
||||
if (socketRef.current && conversationId) {
|
||||
socketRef.current.emit('join_conversation', { conversationId });
|
||||
return () => {
|
||||
socketRef.current?.emit('leave_conversation', { conversationId });
|
||||
};
|
||||
}
|
||||
}, [conversationId]);
|
||||
|
||||
// Load character and conversation
|
||||
useEffect(() => {
|
||||
if (characterId) {
|
||||
getCharacter(characterId);
|
||||
// Create new conversation
|
||||
createConversation({ characterId }).then((conv) => {
|
||||
navigate(`/conversations/${conv.id}`, { replace: true });
|
||||
});
|
||||
} else if (conversationId) {
|
||||
getConversation(conversationId);
|
||||
}
|
||||
}, [characterId, conversationId, getCharacter, createConversation, getConversation, navigate]);
|
||||
|
||||
// Scroll to bottom when messages change
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [currentConversation?.messages, streamingContent]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!message.trim() || !conversationId || isStreaming) return;
|
||||
|
||||
const content = message.trim();
|
||||
setMessage('');
|
||||
setStreamingContent('');
|
||||
setStreaming(true);
|
||||
|
||||
// Send via socket for streaming
|
||||
socketRef.current?.emit('send_message', {
|
||||
conversationId,
|
||||
content,
|
||||
});
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
const formatTime = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleTimeString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
const messages = currentConversation?.messages || [];
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col bg-gray-50">
|
||||
<header className="bg-white shadow flex-shrink-0">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex justify-between items-center">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link to="/conversations" className="text-gray-600 hover:text-gray-900">
|
||||
← Back
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold text-gray-900">
|
||||
{currentConversation?.title || 'Chat'}
|
||||
</h1>
|
||||
{currentCharacter && (
|
||||
<p className="text-sm text-gray-500">
|
||||
with {currentCharacter.name}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="text-sm text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 overflow-hidden flex flex-col max-w-7xl mx-auto w-full px-4 sm:px-6 lg:px-8 py-4">
|
||||
{error && (
|
||||
<div className="mb-4 bg-red-50 text-red-700 p-3 rounded flex-shrink-0">
|
||||
{error}
|
||||
<button onClick={clearError} className="ml-2 text-sm underline">Dismiss</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 overflow-y-auto bg-white rounded-lg shadow p-4 space-y-4">
|
||||
{messages.length === 0 && !isStreaming && (
|
||||
<div className="text-center text-gray-500 py-8">
|
||||
<p>Start a conversation with {currentCharacter?.name || 'your character'}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messages.map((msg) => (
|
||||
<div
|
||||
key={msg.id}
|
||||
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[70%] rounded-lg px-4 py-2 ${
|
||||
msg.role === 'user'
|
||||
? 'bg-indigo-600 text-white'
|
||||
: 'bg-gray-100 text-gray-900'
|
||||
}`}
|
||||
>
|
||||
<p className="whitespace-pre-wrap">{msg.content}</p>
|
||||
<span className={`text-xs ${msg.role === 'user' ? 'text-indigo-200' : 'text-gray-500'} block mt-1`}>
|
||||
{formatTime(msg.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isStreaming && streamingContent && (
|
||||
<div className="flex justify-start">
|
||||
<div className="max-w-[70%] rounded-lg px-4 py-2 bg-gray-100 text-gray-900">
|
||||
<p className="whitespace-pre-wrap">{streamingContent}</p>
|
||||
<span className="text-xs text-gray-500 block mt-1">typing...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="mt-4 flex-shrink-0 flex space-x-4">
|
||||
<input
|
||||
type="text"
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
placeholder="Type your message..."
|
||||
disabled={isStreaming}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent disabled:opacity-50"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!message.trim() || isStreaming}
|
||||
className="px-6 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isStreaming ? 'Sending...' : 'Send'}
|
||||
</button>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
130
apps/frontend/src/pages/ConversationList.tsx
Normal file
130
apps/frontend/src/pages/ConversationList.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useChatStore } from '../stores/chatStore';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
|
||||
export function ConversationList() {
|
||||
const navigate = useNavigate();
|
||||
const { logout } = useAuthStore();
|
||||
const { conversations, isLoading, error, fetchConversations, deleteConversation, clearError } = useChatStore();
|
||||
|
||||
useEffect(() => {
|
||||
fetchConversations();
|
||||
}, [fetchConversations]);
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('Are you sure you want to delete this conversation?')) return;
|
||||
try {
|
||||
await deleteConversation(id);
|
||||
} catch {
|
||||
// Error is handled by the store
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<header className="bg-white shadow">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold text-gray-900">DreamChat</h1>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link
|
||||
to="/characters"
|
||||
className="text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
Characters
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="text-sm text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl font-semibold text-gray-900">My Conversations</h2>
|
||||
<Link
|
||||
to="/characters"
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700"
|
||||
>
|
||||
New Conversation
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 bg-red-50 text-red-700 p-3 rounded">
|
||||
{error}
|
||||
<button onClick={clearError} className="ml-2 text-sm underline">Dismiss</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8">Loading...</div>
|
||||
) : conversations.length === 0 ? (
|
||||
<div className="text-center py-12 bg-white rounded-lg shadow">
|
||||
<p className="text-gray-500">No conversations yet.</p>
|
||||
<Link to="/characters" className="text-indigo-600 hover:text-indigo-500 mt-2 inline-block">
|
||||
Start a conversation with a character
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{conversations.map((conversation) => (
|
||||
<div key={conversation.id} className="bg-white rounded-lg shadow overflow-hidden">
|
||||
<div className="p-6 flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-3">
|
||||
<h3 className="text-lg font-medium text-gray-900">
|
||||
{conversation.title || 'Untitled Conversation'}
|
||||
</h3>
|
||||
{conversation.character && (
|
||||
<span className="text-sm text-gray-500">
|
||||
with {conversation.character.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
{conversation.messageCount} messages • Last updated {formatDate(conversation.updatedAt)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<Link
|
||||
to={`/conversations/${conversation.id}`}
|
||||
className="px-4 py-2 border border-transparent rounded-md text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700"
|
||||
>
|
||||
Continue
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => handleDelete(conversation.id)}
|
||||
className="px-4 py-2 border border-red-300 rounded-md text-sm font-medium text-red-700 hover:bg-red-50"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
212
apps/frontend/src/pages/Login.tsx
Normal file
212
apps/frontend/src/pages/Login.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import { authControllerGetKeycloakConfig, authControllerKeycloakLogin } from '../api/generated/auth/auth';
|
||||
import type { KeycloakConfigDto } from '../api/generated/model';
|
||||
|
||||
interface OAuthError {
|
||||
message: string;
|
||||
isGroupError: boolean;
|
||||
isRoleError: boolean;
|
||||
}
|
||||
|
||||
function parseOAuthError(errorMsg: string): OAuthError {
|
||||
const isGroupError = errorMsg.includes("required group");
|
||||
const isRoleError = errorMsg.includes("required role");
|
||||
|
||||
return {
|
||||
message: errorMsg,
|
||||
isGroupError,
|
||||
isRoleError,
|
||||
};
|
||||
}
|
||||
|
||||
export function Login() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { login, isLoading, error, clearError } = useAuthStore();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [keycloakConfig, setKeycloakConfig] = useState<KeycloakConfigDto | null>(null);
|
||||
const [keycloakError, setKeycloakError] = useState<OAuthError | null>(null);
|
||||
|
||||
// Handle OAuth errors from URL (passed by OAuthCallbackHandler)
|
||||
useEffect(() => {
|
||||
const errorMsg = searchParams.get('error');
|
||||
if (errorMsg) {
|
||||
const decodedError = decodeURIComponent(errorMsg);
|
||||
setKeycloakError(parseOAuthError(decodedError));
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
// Check if Keycloak is enabled
|
||||
useEffect(() => {
|
||||
const checkKeycloak = async () => {
|
||||
try {
|
||||
const config = await authControllerGetKeycloakConfig();
|
||||
// Only show Keycloak button if enabled and properly configured
|
||||
if (config.enabled && config.url && config.realm) {
|
||||
setKeycloakConfig(config);
|
||||
} else {
|
||||
setKeycloakConfig(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Keycloak config error:', error);
|
||||
setKeycloakConfig(null);
|
||||
}
|
||||
};
|
||||
checkKeycloak();
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
clearError();
|
||||
setKeycloakError(null);
|
||||
try {
|
||||
await login({ email, password });
|
||||
navigate('/characters');
|
||||
} catch {
|
||||
// Error is handled by the store
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeycloakLogin = async () => {
|
||||
setKeycloakError(null);
|
||||
try {
|
||||
const response = await authControllerKeycloakLogin({ redirectTo: '/characters' });
|
||||
// Redirect to Keycloak login page
|
||||
window.location.href = response.loginUrl;
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : 'Failed to initiate Keycloak login';
|
||||
setKeycloakError(parseOAuthError(msg));
|
||||
}
|
||||
};
|
||||
|
||||
// Get user-friendly error message
|
||||
const getErrorDisplay = (): { title: string; message: string } | null => {
|
||||
if (keycloakError) {
|
||||
if (keycloakError.isGroupError) {
|
||||
return {
|
||||
title: 'Access Denied',
|
||||
message: 'You do not have the required group membership to access this application. Please contact your administrator.',
|
||||
};
|
||||
}
|
||||
if (keycloakError.isRoleError) {
|
||||
return {
|
||||
title: 'Access Denied',
|
||||
message: 'You do not have the required role to access this application. Please contact your administrator.',
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: 'Authentication Failed',
|
||||
message: keycloakError.message,
|
||||
};
|
||||
}
|
||||
if (error) {
|
||||
return {
|
||||
title: 'Login Failed',
|
||||
message: error,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const errorDisplay = getErrorDisplay();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-center text-gray-900">DreamChat</h1>
|
||||
<h2 className="mt-6 text-2xl font-bold text-center text-gray-900">Sign in to your account</h2>
|
||||
</div>
|
||||
|
||||
{errorDisplay && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-700 p-4 rounded-lg">
|
||||
<h3 className="font-semibold text-red-800 mb-1">{errorDisplay.title}</h3>
|
||||
<p className="text-sm">{errorDisplay.message}</p>
|
||||
{(keycloakError?.isGroupError || keycloakError?.isRoleError) && (
|
||||
<p className="text-xs text-red-600 mt-2">
|
||||
Technical details: {keycloakError.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? 'Signing in...' : 'Sign in'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Keycloak Login Button */}
|
||||
{keycloakConfig && (
|
||||
<div className="mt-6">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-gray-300" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="px-2 bg-white text-gray-500">Or continue with</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleKeycloakLogin}
|
||||
className="mt-4 w-full flex justify-center items-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
<svg
|
||||
className="h-5 w-5 mr-2"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
Sign in with Keycloak
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
131
apps/frontend/src/stores/authStore.ts
Normal file
131
apps/frontend/src/stores/authStore.ts
Normal 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,
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
120
apps/frontend/src/stores/characterStore.ts
Normal file
120
apps/frontend/src/stores/characterStore.ts
Normal 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 }),
|
||||
}));
|
||||
144
apps/frontend/src/stores/chatStore.ts
Normal file
144
apps/frontend/src/stores/chatStore.ts
Normal 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 }),
|
||||
}));
|
||||
55
apps/frontend/src/types/index.ts
Normal file
55
apps/frontend/src/types/index.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
// Character type (not generated because Prisma types aren't in OpenAPI)
|
||||
export interface Character {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl: string | null;
|
||||
personalityPrompt: string;
|
||||
attributes: Record<string, any>;
|
||||
config: Record<string, any>;
|
||||
isPublic: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
// Conversation with messages
|
||||
export interface Message {
|
||||
id: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
tokensUsed: number | null;
|
||||
model: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Conversation {
|
||||
id: string;
|
||||
title: string | null;
|
||||
characterId: string;
|
||||
messageCount: number;
|
||||
totalTokens: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
character?: {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ConversationWithMessages extends Conversation {
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
// Re-export generated types
|
||||
export type {
|
||||
LoginDto,
|
||||
AuthResponseDto,
|
||||
UserDto,
|
||||
CreateCharacterDto,
|
||||
UpdateCharacterDto,
|
||||
CreateConversationDto,
|
||||
SendMessageDto,
|
||||
SendMessageResponseDto,
|
||||
MessageResponseDto,
|
||||
} from '../api/generated/model';
|
||||
10
apps/frontend/src/vite-env.d.ts
vendored
Normal file
10
apps/frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_URL: string;
|
||||
readonly VITE_WS_URL: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
Reference in New Issue
Block a user