From 9d7826a6852405270b6f6d18d5c10e4a3eebcb84 Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Sun, 24 Aug 2025 10:40:23 -0300 Subject: [PATCH 01/56] feat: implement user service and hooks for user creation and login --- src/services/user.ts | 53 ++++++++++++++++++++++++++++++++++++ src/shared/hooks/use-user.ts | 22 +++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 src/services/user.ts create mode 100644 src/shared/hooks/use-user.ts diff --git a/src/services/user.ts b/src/services/user.ts new file mode 100644 index 0000000..3f13461 --- /dev/null +++ b/src/services/user.ts @@ -0,0 +1,53 @@ +import { api } from '@/shared/api' + +export type CreateUserRequest = { + name: string + email: string + password: string + cellphone: string + personType: string + cpfCnpj: string + birthDate?: number // seconds since epoch (optional for business) + plan: string +} + +export type LoginUserRequest = { + email: string + password: string +} + +export type LoginUserResponse = { + id_token: string + access_token: string + refresh_token: string +} + +export const userService = { + async login(request: LoginUserRequest): Promise { + const response = await api.post('/auth', { + email: request.email, + password: request.password + }) + return response.data + }, + + async createUser(request: CreateUserRequest): Promise { + const payload: Record = { + name: request.name, + email: request.email, + password: request.password, + cellphone: request.cellphone, + p_type: request.personType, + cpf_cnpj: request.cpfCnpj, + address: 'Rua x, 123', // Placeholder address + cep: '00000000', // Placeholder CEP + plan: request.plan + } + + if (typeof request.birthDate === 'number') { + payload.birth_date = request.birthDate + } + + await api.post('/user', payload) + } +} diff --git a/src/shared/hooks/use-user.ts b/src/shared/hooks/use-user.ts new file mode 100644 index 0000000..4441b7f --- /dev/null +++ b/src/shared/hooks/use-user.ts @@ -0,0 +1,22 @@ +import { + CreateUserRequest, + LoginUserRequest, + userService +} from '@/services/user' +import { useMutation } from '@tanstack/react-query' + +export const useCreateUserMutation = () => { + return useMutation({ + mutationFn: async (request: CreateUserRequest) => { + return await userService.createUser(request) + } + }) +} + +export const useLoginUserMutation = () => { + return useMutation({ + mutationFn: async (request: LoginUserRequest) => { + return await userService.login(request) + } + }) +} From 752ac255fa28f0ba12c3c089444ae849c07ee714 Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Sun, 24 Aug 2025 10:40:29 -0300 Subject: [PATCH 02/56] feat: enhance login functionality with user mutation and loading state --- src/features/login/pages/login.tsx | 32 +++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/features/login/pages/login.tsx b/src/features/login/pages/login.tsx index e6de017..6a8ad8f 100644 --- a/src/features/login/pages/login.tsx +++ b/src/features/login/pages/login.tsx @@ -28,11 +28,16 @@ import { Form } from '@/shared/components/form' import { Input } from '@/shared/components/input' -import { Link } from 'react-router-dom' +import { Link, useNavigate } from 'react-router-dom' import { containerVariants, cardVariants } from '@/shared/utils/animations' +import { useLoginUserMutation } from '@/shared/hooks/use-user' +import toast from 'react-hot-toast' +import { AxiosError } from 'axios' export function LoginPage() { const [showPassword, setShowPassword] = useState(false) + const { mutateAsync: loginUser, isPending } = useLoginUserMutation() + const navigate = useNavigate() const form = useForm({ resolver: zodResolver(loginFormSchema), @@ -40,8 +45,21 @@ export function LoginPage() { mode: 'onBlur' }) - const onSubmit = (values: LoginFormData) => { - console.log(values) + const onSubmit = async (values: LoginFormData) => { + try { + const response = await loginUser(values) + localStorage.setItem('access_token', response.access_token) + toast.success('Login realizado com sucesso!') + navigate('/bases', { + replace: true + }) + } catch (err) { + const errorMessage = + err instanceof AxiosError && err.response?.data?.message + ? err.response.data.message + : 'Erro ao fazer login' + toast.error(errorMessage) + } } return ( @@ -116,8 +134,12 @@ export function LoginPage() { )} /> -
Ainda não tem uma conta?{' '} From 5270673e3c990b37ac16eba7c03ac6b000990181 Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Sun, 24 Aug 2025 10:40:31 -0300 Subject: [PATCH 03/56] feat: implement user creation logic and loading state in sign-up page --- src/features/sign-up/pages/sign-up-page.tsx | 46 +++++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/src/features/sign-up/pages/sign-up-page.tsx b/src/features/sign-up/pages/sign-up-page.tsx index e3dc52a..7107a90 100644 --- a/src/features/sign-up/pages/sign-up-page.tsx +++ b/src/features/sign-up/pages/sign-up-page.tsx @@ -38,10 +38,16 @@ import { motion } from 'framer-motion' import { Eye, EyeOff } from 'lucide-react' import { useState } from 'react' import { containerVariants, cardVariants } from '@/shared/utils/animations' +import { useCreateUserMutation } from '@/shared/hooks/use-user' +import { AxiosError } from 'axios' +import { useNavigate } from 'react-router-dom' export function SignUpPage() { const [showPassword, setShowPassword] = useState(false) const [showConfirmPassword, setShowConfirmPassword] = useState(false) + const navigate = useNavigate() + + const { mutateAsync: createUser, isPending } = useCreateUserMutation() const form = useForm({ resolver: zodResolver(signUpFormSchema), @@ -73,9 +79,39 @@ export function SignUpPage() { onChange(formattedValue) } - function onSubmit(values: SignUpFormData) { - console.log(values) - toast.success('Conta criada com sucesso!') + async function onSubmit(values: SignUpFormData) { + try { + const isIndividual = values.documentType === 'individual' + + // Convert birthDate to seconds since epoch if provided + const birthDateSeconds = values.birthDate + ? Math.floor(values.birthDate.getTime() / 1000) + : undefined + + await createUser({ + name: values.name, + email: values.email, + password: values.password, + cellphone: values.phone, + personType: isIndividual ? 'PF' : 'PJ', + cpfCnpj: values.document.replace(/\D/g, ''), + birthDate: birthDateSeconds, + plan: 'Bronze' + }) + + form.reset() + toast.success( + 'Conta criada! Confirme seu cadastro pelo link enviado para o seu e-mail. Você será redirecionado para a página de login.', + { duration: 10000 } + ) + setTimeout(() => { + navigate('/login', { replace: true }) + }, 10000) + } catch (err) { + const errorMessage = + ((err as AxiosError).response?.data as string) || 'Erro ao fazer login' + toast.error(errorMessage) + } } return ( @@ -326,7 +362,9 @@ export function SignUpPage() { /> - + From 16a455ce6217f4bd12aa2d68b34ca0b097aa3e2e Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Sun, 24 Aug 2025 10:40:37 -0300 Subject: [PATCH 04/56] feat: update navbar login button to conditionally navigate based on user authentication --- src/shared/components/navbar.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/shared/components/navbar.tsx b/src/shared/components/navbar.tsx index 2ec3656..e77f90b 100644 --- a/src/shared/components/navbar.tsx +++ b/src/shared/components/navbar.tsx @@ -23,6 +23,9 @@ export function Navbar() { const { theme, setTheme } = useTheme() const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false) + // Determine if user is logged in based on presence of token in localStorage + const isLoggedIn = Boolean(localStorage.getItem('access_token')) + const toggleMobileMenu = () => { setIsMobileMenuOpen(!isMobileMenuOpen) } @@ -106,9 +109,9 @@ export function Navbar() { @@ -142,9 +145,9 @@ export function Navbar() {
{/* Chat Container */} - + From 7a11058a63b8cfa12444769e82196bcda69b8a07 Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Mon, 6 Oct 2025 23:01:17 -0300 Subject: [PATCH 13/56] feat: add user query and context hooks for user management --- src/shared/hooks/use-user.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/shared/hooks/use-user.ts b/src/shared/hooks/use-user.ts index 4441b7f..a920e41 100644 --- a/src/shared/hooks/use-user.ts +++ b/src/shared/hooks/use-user.ts @@ -3,7 +3,9 @@ import { LoginUserRequest, userService } from '@/services/user' -import { useMutation } from '@tanstack/react-query' +import { UserContext } from '@/shared/contexts/user-context' +import { useMutation, useQuery } from '@tanstack/react-query' +import { useContext } from 'react' export const useCreateUserMutation = () => { return useMutation({ @@ -20,3 +22,20 @@ export const useLoginUserMutation = () => { } }) } + +export const useUserQuery = () => { + return useQuery({ + queryKey: ['user'], + queryFn: async () => { + return await userService.getUser() + } + }) +} + +export const useUser = () => { + const context = useContext(UserContext) + if (!context) { + throw new Error('useUser must be used within a UserProvider') + } + return context +} From 49782c372f3836dd793e11d15cd8b4a3a060d01b Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Mon, 6 Oct 2025 23:01:28 -0300 Subject: [PATCH 14/56] feat: update model identifiers to use constant values --- src/shared/enums/models.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/enums/models.ts b/src/shared/enums/models.ts index 953a507..e20a5fb 100644 --- a/src/shared/enums/models.ts +++ b/src/shared/enums/models.ts @@ -1,8 +1,8 @@ import { ObjectValues } from '@/shared/enums/object-values' export const MODELS = { - MISTRAL_SMALL: 'mistral.mistral-small-2402-v1:0', - AMAZON_NOVA_MICRO: 'amazon.nova-micro-v1:0' + MISTRAL_SMALL: 'MISTRAL_SMALL', + AMAZON_NOVA_MICRO: 'AMAZON_NOVA_MICRO' } as const export type MODELS = ObjectValues From 6b980785548cdb5a6dcd5b9a102b447c72b54b8f Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Mon, 6 Oct 2025 23:01:33 -0300 Subject: [PATCH 15/56] feat: update document type values to use constant identifiers --- src/shared/enums/document-type.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/enums/document-type.ts b/src/shared/enums/document-type.ts index 6ebfc3c..56ea732 100644 --- a/src/shared/enums/document-type.ts +++ b/src/shared/enums/document-type.ts @@ -1,8 +1,8 @@ import { ObjectValues } from '@/shared/enums/object-values' export const DOCUMENT_TYPE = { - INDIVIDUAL: 'individual', - BUSINESS: 'business' + INDIVIDUAL: 'PF', + BUSINESS: 'PJ' } as const export type DOCUMENT_TYPE = ObjectValues From 07bf122903fe3e2c0614224dce1d8080948b5fef Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Mon, 6 Oct 2025 23:01:38 -0300 Subject: [PATCH 16/56] feat: add User type definition for user domain model --- src/shared/domain/user.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/shared/domain/user.ts diff --git a/src/shared/domain/user.ts b/src/shared/domain/user.ts new file mode 100644 index 0000000..e549701 --- /dev/null +++ b/src/shared/domain/user.ts @@ -0,0 +1,14 @@ +export type User = { + id: string + name: string + email: string + cellphone: string + personType: string + cpfCnpj: string + address: string + cep: string + birthDate?: Date + plan: string + creationDate: number + updateDate: number +} From b50007c396d763be57ed99a1e7efc6978a20ff92 Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Mon, 6 Oct 2025 23:01:45 -0300 Subject: [PATCH 17/56] fix: update token retrieval in getAuthHeader to use correct localStorage key --- src/shared/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/api.ts b/src/shared/api.ts index 86bca9a..5d15660 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -10,7 +10,7 @@ export const api = axios.create({ }) export const getAuthHeader = () => { - const token = localStorage.getItem('access_token') || '' + const token = localStorage.getItem('token') || '' return { Authorization: `Bearer ${token}` } From 043c9a8c1d5ed6d437339b33046435b50d0fdba9 Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Mon, 6 Oct 2025 23:01:51 -0300 Subject: [PATCH 18/56] feat: wrap PageWithSidebar component in UserProvider for context management --- src/shared/components/page-with-sidebar.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/shared/components/page-with-sidebar.tsx b/src/shared/components/page-with-sidebar.tsx index 2d64267..511a284 100644 --- a/src/shared/components/page-with-sidebar.tsx +++ b/src/shared/components/page-with-sidebar.tsx @@ -2,14 +2,15 @@ import { Outlet } from 'react-router-dom' import { Navbar } from '@/shared/components/navbar' import { Toaster } from 'react-hot-toast' import { Sidebar } from '@/shared/components/sidebar' +import { UserProvider } from '@/shared/contexts/user-context' export function PageWithSidebar() { return ( - <> + - + ) } From 0d83485f4cbbaa51f17d12d5a8000f2de88f9b8f Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Mon, 6 Oct 2025 23:01:58 -0300 Subject: [PATCH 19/56] feat: implement UserContext and UserProvider for user state management --- src/shared/contexts/user-context.tsx | 47 ++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/shared/contexts/user-context.tsx diff --git a/src/shared/contexts/user-context.tsx b/src/shared/contexts/user-context.tsx new file mode 100644 index 0000000..e3fc7fb --- /dev/null +++ b/src/shared/contexts/user-context.tsx @@ -0,0 +1,47 @@ +import { User } from '@/shared/domain/user' +import { useUserQuery } from '@/shared/hooks/use-user' +import { createContext, useState, useEffect, ReactNode } from 'react' + +interface UserContextType { + user: User | null + setUser: (user: User | null) => void + isAuthenticated: boolean + logout: () => void + isPending?: boolean + error?: unknown +} + +export const UserContext = createContext(undefined) + +interface UserProviderProps { + children: ReactNode +} + +export const UserProvider = ({ children }: UserProviderProps) => { + const [user, setUser] = useState(null) + const { data: userData, isPending, error } = useUserQuery() + + const isAuthenticated = user !== null + + const logout = () => { + setUser(null) + } + + useEffect(() => { + // Set user from query data + if (userData) { + setUser(userData) + } + }, [userData]) + + const value: UserContextType = { + user, + setUser, + isAuthenticated, + logout, + isPending, + error + } + + return {children} +} From 0cf960c64c7daf35b242d0c521e5a130754ae3e6 Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Mon, 6 Oct 2025 23:02:11 -0300 Subject: [PATCH 20/56] feat: add user response type and implement getUser method in userService --- src/services/user.ts | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/services/user.ts b/src/services/user.ts index 3f13461..0625fc7 100644 --- a/src/services/user.ts +++ b/src/services/user.ts @@ -1,4 +1,5 @@ -import { api } from '@/shared/api' +import { api, getAuthHeader } from '@/shared/api' +import { User } from '@/shared/domain/user' export type CreateUserRequest = { name: string @@ -22,6 +23,23 @@ export type LoginUserResponse = { refresh_token: string } +export type UserResponse = { + user: { + user_id: string + name: string + email: string + cellphone: string + p_type: string + cpf_cnpj: string + address: string + cep: string + birthdate: number | null + plan: string + creation_date: number + update_date: number + } +} + export const userService = { async login(request: LoginUserRequest): Promise { const response = await api.post('/auth', { @@ -49,5 +67,26 @@ export const userService = { } await api.post('/user', payload) + }, + + async getUser(): Promise { + const response = await api.get('/user', { + headers: getAuthHeader() + }) + const data = response.data.user + return { + id: data.user_id, + name: data.name, + email: data.email, + cellphone: data.cellphone, + personType: data.p_type, + cpfCnpj: data.cpf_cnpj, + address: data.address, + cep: data.cep, + birthDate: data.birthdate ? new Date(data.birthdate) : undefined, + plan: data.plan, + creationDate: data.creation_date, + updateDate: data.update_date + } } } From d1857dceb9c16a9934f49cdaf51ec1228d93d659 Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Mon, 6 Oct 2025 23:02:33 -0300 Subject: [PATCH 21/56] refactor: rename fields in userInfoSchema for consistency and clarity --- src/features/user-area/types/user-info-schema.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/features/user-area/types/user-info-schema.ts b/src/features/user-area/types/user-info-schema.ts index 42b60ae..78a86c4 100644 --- a/src/features/user-area/types/user-info-schema.ts +++ b/src/features/user-area/types/user-info-schema.ts @@ -9,10 +9,10 @@ export const userInfoSchema = z .min(4, { message: 'O nome deve ter pelo menos 4 caracteres.' }) .max(50, { message: 'O nome deve ter no máximo 50 caracteres.' }), email: z.string().email({ message: 'O e-mail deve ser válido.' }), - phone: z + cellphone: z .string() .min(11, { message: 'O telefone deve ter pelo menos 11 dígitos.' }), - documentType: z + personType: z .string() .refine( (val) => Object.values(DOCUMENT_TYPE).includes(val as DOCUMENT_TYPE), @@ -20,7 +20,7 @@ export const userInfoSchema = z message: 'Selecione um tipo de pessoa válido.' } ), - document: z.string().min(1, { message: 'O documento é obrigatório.' }), + cpfCnpj: z.string().min(1, { message: 'O documento é obrigatório.' }), birthDate: z .date() .refine((date) => !isNaN(date.getTime()), { @@ -38,7 +38,7 @@ export const userInfoSchema = z .optional() }) .superRefine((data, ctx) => { - if (data.documentType === 'individual' && !data.birthDate) { + if (data.personType === DOCUMENT_TYPE.INDIVIDUAL && !data.birthDate) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'A data de nascimento é obrigatória para pessoa física.', @@ -46,20 +46,20 @@ export const userInfoSchema = z }) } - if (data.documentType === 'individual') { + if (data.personType === DOCUMENT_TYPE.INDIVIDUAL) { // Validação CPF const cpfRegex = /^\d{3}\.?\d{3}\.?\d{3}-?\d{2}$/ - if (!cpfRegex.test(data.document)) { + if (!cpfRegex.test(data.cpfCnpj)) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'CPF inválido. Use o formato: 000.000.000-00', path: ['document'] }) } - } else if (data.documentType === 'business') { + } else if (data.personType === DOCUMENT_TYPE.BUSINESS) { // Validação CNPJ const cnpjRegex = /^(\d{2}\.?\d{3}\.?\d{3}\/?\d{4}-?\d{2})$/ - if (!cnpjRegex.test(data.document)) { + if (!cnpjRegex.test(data.cpfCnpj)) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'CNPJ inválido. Use o formato: 00.000.000/0000-00', From a2f06d23902c40a88417f8beaee8ef08e49d8bd9 Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Mon, 6 Oct 2025 23:02:37 -0300 Subject: [PATCH 22/56] style: adjust padding for message content in PlaygroundPage component --- src/features/playground/pages/playground.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/playground/pages/playground.tsx b/src/features/playground/pages/playground.tsx index bb064a7..202435d 100644 --- a/src/features/playground/pages/playground.tsx +++ b/src/features/playground/pages/playground.tsx @@ -231,7 +231,7 @@ export function PlaygroundPage() { : 'bg-muted text-foreground' }`} > -

+

{message.content}

From 60f75fb64410addd550cc423bffa22f0d8b5d6d0 Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Mon, 6 Oct 2025 23:02:54 -0300 Subject: [PATCH 23/56] fix: update token storage key from 'access_token' to 'token' in LoginPage --- src/features/login/pages/login.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/login/pages/login.tsx b/src/features/login/pages/login.tsx index 6a8ad8f..5ee33b5 100644 --- a/src/features/login/pages/login.tsx +++ b/src/features/login/pages/login.tsx @@ -48,7 +48,7 @@ export function LoginPage() { const onSubmit = async (values: LoginFormData) => { try { const response = await loginUser(values) - localStorage.setItem('access_token', response.access_token) + localStorage.setItem('token', response.id_token) toast.success('Login realizado com sucesso!') navigate('/bases', { replace: true From d13e6cb44cae0c9d12b1309fb7e6db8446b8eab9 Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Mon, 6 Oct 2025 23:03:02 -0300 Subject: [PATCH 24/56] refactor: replace mock user with useUser hook and update form fields for user info --- src/features/user-area/pages/user-info.tsx | 95 +++++++++++----------- 1 file changed, 49 insertions(+), 46 deletions(-) diff --git a/src/features/user-area/pages/user-info.tsx b/src/features/user-area/pages/user-info.tsx index 221996b..685c87c 100644 --- a/src/features/user-area/pages/user-info.tsx +++ b/src/features/user-area/pages/user-info.tsx @@ -29,21 +29,28 @@ import { formatPhone } from '@/shared/utils/format-documents' import { Background } from '@/shared/components/background' - -// Usuário simulado (como se estivesse logado) -const mockUser: UserInfoData & { id: string } = { - id: '1', - name: 'Maria da Silva', - email: 'maria@email.com', - documentType: DOCUMENT_TYPE.INDIVIDUAL, - phone: formatPhone('11912345678'), - document: '234.781.938-47', - birthDate: new Date(2001, 7, 12) // Começa no mês 0 -} +import { useUser } from '@/shared/hooks/use-user' export function UserInfoPage() { const [isEditing, setIsEditing] = useState(false) - const [user, setUser] = useState(mockUser) + const { user, isPending } = useUser() + + const form = useForm({ + resolver: zodResolver(userInfoSchema), + defaultValues: { + name: user?.name || '', + email: user?.email || '', + cellphone: user?.cellphone || '', + personType: user?.personType || 'individual', + cpfCnpj: user?.cpfCnpj || '', + birthDate: user?.birthDate + }, + mode: 'onBlur' + }) + + if (isPending || !user) { + return

Loading
+ } const handleCPFChange = ( value: string, @@ -69,48 +76,44 @@ export function UserInfoPage() { onChange(formattedValue) } - const form = useForm({ - resolver: zodResolver(userInfoSchema), - defaultValues: { - name: user.name, - email: user.email, - phone: user.phone, - documentType: user.documentType, - document: user.document, - birthDate: user.birthDate - }, - mode: 'onBlur' - }) - function handleEdit() { + if (!user) return + setIsEditing(true) form.reset({ name: user.name, email: user.email, - phone: user.phone, - documentType: user.documentType, - document: user.document, + cellphone: user.cellphone, + personType: user.personType, + cpfCnpj: user.cpfCnpj, birthDate: user.birthDate }) } function handleCancel() { + if (!user) return + setIsEditing(false) form.reset({ name: user.name, email: user.email, - phone: user.phone, - documentType: user.documentType, - document: user.document, + cellphone: user.cellphone, + personType: user.personType, + cpfCnpj: user.cpfCnpj, birthDate: user.birthDate }) } function onSubmit(data: UserInfoData) { - setUser({ - ...user, - ...data - }) + const dirtyFields = form.formState.dirtyFields + const changedData = Object.keys(dirtyFields).reduce((acc, key) => { + if (dirtyFields[key as keyof UserInfoData]) { + acc[key as keyof UserInfoData] = data[key as keyof UserInfoData] + } + return acc + }, {} as Partial) + + console.log('Changed fields:', changedData) setIsEditing(false) } @@ -140,13 +143,13 @@ export function UserInfoPage() {
- +
Documento @@ -214,7 +217,7 @@ export function UserInfoPage() { /> ( Telefone/Celular @@ -235,7 +238,7 @@ export function UserInfoPage() { /> ( Tipo de Pessoa @@ -244,7 +247,7 @@ export function UserInfoPage() { ( Documento @@ -272,7 +275,7 @@ export function UserInfoPage() { {...field} onChange={(e) => { if ( - form.getValues('documentType') === + form.getValues('personType') === 'individual' ) { handleCPFChange( @@ -292,7 +295,7 @@ export function UserInfoPage() { )} /> - {user.documentType === DOCUMENT_TYPE.INDIVIDUAL && ( + {user.personType === DOCUMENT_TYPE.INDIVIDUAL && ( Date: Mon, 6 Oct 2025 23:13:26 -0300 Subject: [PATCH 25/56] feat: Add environment variables for AWS deployment --- .github/workflows/aws-cd.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/aws-cd.yml b/.github/workflows/aws-cd.yml index f03aeb4..a2206cc 100644 --- a/.github/workflows/aws-cd.yml +++ b/.github/workflows/aws-cd.yml @@ -40,6 +40,8 @@ jobs: npm run build env: VITE_STAGE: ${{ github.ref_name }} + VITE_API_URL: ${{vars.API_URL}} + VITE_BUCKET_NAME: ${{vars.BUCKET_NAME}} - name: Setup envs run: | echo AWS_REGION=${{ vars.AWS_REGION }} >> $GITHUB_ENV From 061dd4a2dca58ea57c68a089528f2fe3505b18bc Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Mon, 6 Oct 2025 23:23:59 -0300 Subject: [PATCH 26/56] feat: add useUpdateUserMutation hook to handle user updates --- src/shared/hooks/use-user.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/shared/hooks/use-user.ts b/src/shared/hooks/use-user.ts index a920e41..48c47c3 100644 --- a/src/shared/hooks/use-user.ts +++ b/src/shared/hooks/use-user.ts @@ -1,6 +1,7 @@ import { CreateUserRequest, LoginUserRequest, + UpdateUserRequest, userService } from '@/services/user' import { UserContext } from '@/shared/contexts/user-context' @@ -32,6 +33,14 @@ export const useUserQuery = () => { }) } +export const useUpdateUserMutation = () => { + return useMutation({ + mutationFn: async (request: UpdateUserRequest) => { + return await userService.updateUser(request) + } + }) +} + export const useUser = () => { const context = useContext(UserContext) if (!context) { From 372ca6136f1114667768f1ce5506deb58f366a97 Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Mon, 6 Oct 2025 23:24:13 -0300 Subject: [PATCH 27/56] feat: integrate user update functionality with loading state and error handling --- src/features/user-area/pages/user-info.tsx | 43 ++++++++++++++++------ 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/src/features/user-area/pages/user-info.tsx b/src/features/user-area/pages/user-info.tsx index 685c87c..2f7ac09 100644 --- a/src/features/user-area/pages/user-info.tsx +++ b/src/features/user-area/pages/user-info.tsx @@ -29,11 +29,15 @@ import { formatPhone } from '@/shared/utils/format-documents' import { Background } from '@/shared/components/background' -import { useUser } from '@/shared/hooks/use-user' +import { useUpdateUserMutation, useUser } from '@/shared/hooks/use-user' +import { UpdateUserRequest } from '@/services/user' +import toast from 'react-hot-toast' export function UserInfoPage() { const [isEditing, setIsEditing] = useState(false) - const { user, isPending } = useUser() + const { user, isPending, refetch } = useUser() + const { mutateAsync: updateUser, isPending: isUpdating } = + useUpdateUserMutation() const form = useForm({ resolver: zodResolver(userInfoSchema), @@ -104,17 +108,32 @@ export function UserInfoPage() { }) } - function onSubmit(data: UserInfoData) { - const dirtyFields = form.formState.dirtyFields - const changedData = Object.keys(dirtyFields).reduce((acc, key) => { - if (dirtyFields[key as keyof UserInfoData]) { - acc[key as keyof UserInfoData] = data[key as keyof UserInfoData] + async function onSubmit(data: UserInfoData) { + const dirtyFields = form.formState.dirtyFields as Partial< + Record + > + + const changedData: Partial = {} + + function setField(k: K) { + changedData[k] = data[k] + } + + ;(Object.keys(dirtyFields) as (keyof UserInfoData)[]).forEach((key) => { + if (dirtyFields[key]) { + setField(key) } - return acc - }, {} as Partial) + }) - console.log('Changed fields:', changedData) - setIsEditing(false) + try { + await updateUser(changedData as UpdateUserRequest) + refetch?.() + } catch (error) { + console.error('Failed to update user:', error) + toast.error('Erro ao atualizar os dados. Tente novamente.') + } finally { + setIsEditing(false) + } } return ( @@ -328,12 +347,14 @@ export function UserInfoPage() { variant="secondary" className="w-1/2" onClick={handleCancel} + disabled={isUpdating} > Cancelar From 6746cf50eba846d14f177a4a3ed751c2b3d4072f Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Mon, 6 Oct 2025 23:24:17 -0300 Subject: [PATCH 28/56] feat: add updateUser method and adjust birthdate conversion in user service --- src/services/user.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/services/user.ts b/src/services/user.ts index 0625fc7..ad77f0f 100644 --- a/src/services/user.ts +++ b/src/services/user.ts @@ -12,6 +12,12 @@ export type CreateUserRequest = { plan: string } +export type UpdateUserRequest = { + name?: string + cellphone?: string + plan?: string +} + export type LoginUserRequest = { email: string password: string @@ -69,11 +75,24 @@ export const userService = { await api.post('/user', payload) }, + async updateUser(request: UpdateUserRequest): Promise { + const payload = { + new_name: request.name, + new_cellphone: request.cellphone, + new_plan: request.plan + } + + await api.patch('/user', payload, { + headers: getAuthHeader() + }) + }, + async getUser(): Promise { const response = await api.get('/user', { headers: getAuthHeader() }) const data = response.data.user + console.log('User data fetched:', data) return { id: data.user_id, name: data.name, @@ -83,7 +102,7 @@ export const userService = { cpfCnpj: data.cpf_cnpj, address: data.address, cep: data.cep, - birthDate: data.birthdate ? new Date(data.birthdate) : undefined, + birthDate: data.birthdate ? new Date(data.birthdate * 1000) : undefined, plan: data.plan, creationDate: data.creation_date, updateDate: data.update_date From 4c508f00e5ca55265335d3342864020097c436d5 Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Mon, 6 Oct 2025 23:24:21 -0300 Subject: [PATCH 29/56] feat: add refetch functionality to UserContext for improved user data management --- src/shared/contexts/user-context.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/shared/contexts/user-context.tsx b/src/shared/contexts/user-context.tsx index e3fc7fb..ba85427 100644 --- a/src/shared/contexts/user-context.tsx +++ b/src/shared/contexts/user-context.tsx @@ -9,6 +9,7 @@ interface UserContextType { logout: () => void isPending?: boolean error?: unknown + refetch?: () => void } export const UserContext = createContext(undefined) @@ -19,7 +20,7 @@ interface UserProviderProps { export const UserProvider = ({ children }: UserProviderProps) => { const [user, setUser] = useState(null) - const { data: userData, isPending, error } = useUserQuery() + const { data: userData, isPending, error, refetch } = useUserQuery() const isAuthenticated = user !== null @@ -40,7 +41,8 @@ export const UserProvider = ({ children }: UserProviderProps) => { isAuthenticated, logout, isPending, - error + error, + refetch } return {children} From 8b77b083c6424ecb3a3a363cc5f4ba858a22db8d Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Sun, 19 Oct 2025 21:56:03 -0300 Subject: [PATCH 30/56] feat: add API key management to KnowledgeBaseDetailModal and update KnowledgeBase type --- src/domain/knowledge-base.ts | 6 + .../knowledge-base-detail-modal.tsx | 108 +++++++++++++++++- src/services/knowledge-base.ts | 10 +- 3 files changed, 122 insertions(+), 2 deletions(-) diff --git a/src/domain/knowledge-base.ts b/src/domain/knowledge-base.ts index 906bf8f..793a789 100644 --- a/src/domain/knowledge-base.ts +++ b/src/domain/knowledge-base.ts @@ -10,6 +10,7 @@ export type KnowledgeBase = { status: STATUS files: KnowledgeBaseFile[] totalSizeMB: number + keys: KnowledgeBaseKey[] } export type KnowledgeBaseFile = { @@ -17,3 +18,8 @@ export type KnowledgeBaseFile = { sizeMB: number url: string } + +export type KnowledgeBaseKey = { + kbKey: string + kbKeyAlias: string +} diff --git a/src/features/user-area/components/knowledge-base-detail-modal.tsx b/src/features/user-area/components/knowledge-base-detail-modal.tsx index e5aea7f..190f59a 100644 --- a/src/features/user-area/components/knowledge-base-detail-modal.tsx +++ b/src/features/user-area/components/knowledge-base-detail-modal.tsx @@ -1,5 +1,15 @@ import { useNavigate } from 'react-router-dom' -import { FileText, Trash2, Plus, Play } from 'lucide-react' +import { useState } from 'react' +import { + FileText, + Trash2, + Plus, + Play, + Eye, + EyeOff, + Copy, + Check +} from 'lucide-react' import { Dialog, DialogContent, @@ -24,6 +34,8 @@ export function KnowledgeBaseDetailModal({ onOpenChange }: KnowledgeBaseDetailModalProps) { const navigate = useNavigate() + const [visibleKeys, setVisibleKeys] = useState>(new Set()) + const [copiedKeys, setCopiedKeys] = useState>(new Set()) const formatFileSize = (sizeInMB: number) => { if (sizeInMB < 1) { @@ -60,6 +72,38 @@ export function KnowledgeBaseDetailModal({ onOpenChange(false) } + const toggleKeyVisibility = (keyId: string) => { + setVisibleKeys((prev) => { + const newSet = new Set(prev) + if (newSet.has(keyId)) { + newSet.delete(keyId) + } else { + newSet.add(keyId) + } + return newSet + }) + } + + const copyKeyToClipboard = async (key: string, keyId: string) => { + try { + await navigator.clipboard.writeText(key) + setCopiedKeys((prev) => new Set(prev).add(keyId)) + setTimeout(() => { + setCopiedKeys((prev) => { + const newSet = new Set(prev) + newSet.delete(keyId) + return newSet + }) + }, 2000) + } catch (err) { + console.error('Erro ao copiar chave:', err) + } + } + + const maskKey = (key: string) => { + return '•'.repeat(key.length) + } + return ( @@ -107,6 +151,68 @@ export function KnowledgeBaseDetailModal({ + {/* API Keys */} +
+

API Keys

+ {knowledgeBase.keys.length === 0 ? ( +
+ Nenhuma chave de API encontrada +
+ ) : ( +
+ {knowledgeBase.keys.map((key, index) => { + const keyId = `${key.kbKey}-${index}` + const isVisible = visibleKeys.has(keyId) + const isCopied = copiedKeys.has(keyId) + + return ( +
+
+ + {key.kbKeyAlias} + +
+
+
+ {isVisible ? key.kbKey : maskKey(key.kbKey)} +
+ + +
+
+ ) + })} +
+ )} +
+ + + {/* Lista de arquivos */}
diff --git a/src/services/knowledge-base.ts b/src/services/knowledge-base.ts index 0e1c367..d00df78 100644 --- a/src/services/knowledge-base.ts +++ b/src/services/knowledge-base.ts @@ -58,6 +58,10 @@ export type GetKnowledgeBaseResponse = { url: string }[] total_size_mb: number + keys: { + kb_key: string + kb_key_alias: string + }[] }[] } @@ -98,7 +102,11 @@ export const knowledgeBaseService = { sizeMB: file.size_bytes / (1024 * 1024), url: file.url })), - totalSizeMB: kb.total_size_mb + totalSizeMB: kb.total_size_mb, + keys: kb.keys.map((key) => ({ + kbKey: key.kb_key, + kbKeyAlias: key.kb_key_alias + })) } }) }, From 6d1524eb2bbc4489046249d0dca373a47d32e5bb Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Sun, 19 Oct 2025 22:19:18 -0300 Subject: [PATCH 31/56] fix: update token retrieval in Navbar component for user login status --- src/shared/components/navbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/components/navbar.tsx b/src/shared/components/navbar.tsx index e77f90b..50ae156 100644 --- a/src/shared/components/navbar.tsx +++ b/src/shared/components/navbar.tsx @@ -24,7 +24,7 @@ export function Navbar() { const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false) // Determine if user is logged in based on presence of token in localStorage - const isLoggedIn = Boolean(localStorage.getItem('access_token')) + const isLoggedIn = Boolean(localStorage.getItem('token')) const toggleMobileMenu = () => { setIsMobileMenuOpen(!isMobileMenuOpen) From 61ed2a416fec8d5c65f208fccd9a26a4808ae64b Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Sun, 19 Oct 2025 22:19:33 -0300 Subject: [PATCH 32/56] feat: add apiChat for handling chat requests and update environment configuration --- src/services/knowledge-base.ts | 6 ++++-- src/shared/api.ts | 10 +++++++++- src/shared/enviroment.ts | 2 ++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/services/knowledge-base.ts b/src/services/knowledge-base.ts index d00df78..d8ef595 100644 --- a/src/services/knowledge-base.ts +++ b/src/services/knowledge-base.ts @@ -1,5 +1,5 @@ import { KnowledgeBase } from '@/domain/knowledge-base' -import { api, getAuthHeader } from '@/shared/api' +import { api, apiChat, getAuthHeader } from '@/shared/api' import { MODELS } from '@/shared/enums/models' import { STATUS } from '@/shared/enums/status' @@ -67,6 +67,7 @@ export type GetKnowledgeBaseResponse = { export type ChatWithKnowledgeBaseRequest = { kbId: string + kbKey: string model: MODELS prompt: string topK?: number @@ -168,10 +169,11 @@ export const knowledgeBaseService = { async chatWithKnowledgeBase( request: ChatWithKnowledgeBaseRequest ): Promise { - const response = await api.post( + const response = await apiChat.post( '/chat', { kb_id: request.kbId, + kb_key: request.kbKey, model: request.model, prompt: request.prompt, top_k: request.topK diff --git a/src/shared/api.ts b/src/shared/api.ts index 5d15660..5814efd 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -1,4 +1,4 @@ -import { apiUrl } from '@/shared/enviroment' +import { apiChatUrl, apiUrl } from '@/shared/enviroment' import axios from 'axios' export const api = axios.create({ @@ -9,6 +9,14 @@ export const api = axios.create({ } }) +export const apiChat = axios.create({ + baseURL: apiChatUrl, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json' + } +}) + export const getAuthHeader = () => { const token = localStorage.getItem('token') || '' return { diff --git a/src/shared/enviroment.ts b/src/shared/enviroment.ts index 704b4c1..85239dd 100644 --- a/src/shared/enviroment.ts +++ b/src/shared/enviroment.ts @@ -1,4 +1,6 @@ export const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000' +export const apiChatUrl = + import.meta.env.VITE_API_CHAT_URL || 'http://localhost:8001' export const bucketName = import.meta.env.VITE_BUCKET_NAME || 'knowly-knowledge-bases-files' From b261d5c29b561a7fc942df8ed4b4e3508e093366 Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Sun, 19 Oct 2025 22:19:40 -0300 Subject: [PATCH 33/56] fix: update language attribute in HTML to Portuguese (pt-BR) --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index a4a211e..b5d4932 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,5 @@ - + From aeb300e0f965c11a63152ade65933863d2c719d6 Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Sun, 19 Oct 2025 22:19:44 -0300 Subject: [PATCH 34/56] fix: add missing VITE_API_CHAT_URL to ImportMetaEnv interface --- src/vite-env.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 405e855..4d9afd0 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -2,6 +2,7 @@ interface ImportMetaEnv { readonly VITE_API_URL: string + readonly VITE_API_CHAT_URL: string readonly VITE_BUCKET_NAME: string } From b40d0338d37b1f30d96a9b1b2d14ea94fc0cf7e1 Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Sun, 19 Oct 2025 22:19:55 -0300 Subject: [PATCH 35/56] feat: pass first available API key to PlaygroundPage on navigation --- src/features/playground/pages/playground.tsx | 26 ++++++++++++++++++- .../knowledge-base-detail-modal.tsx | 7 ++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/features/playground/pages/playground.tsx b/src/features/playground/pages/playground.tsx index 202435d..34368fa 100644 --- a/src/features/playground/pages/playground.tsx +++ b/src/features/playground/pages/playground.tsx @@ -1,5 +1,5 @@ import { useState, useRef, useEffect } from 'react' -import { useParams, useNavigate } from 'react-router-dom' +import { useParams, useNavigate, useLocation } from 'react-router-dom' import { motion } from 'framer-motion' import { Send, Bot, User, ArrowLeft } from 'lucide-react' import toast from 'react-hot-toast' @@ -36,6 +36,8 @@ type Message = { export function PlaygroundPage() { const { kbId } = useParams<{ kbId: string }>() const navigate = useNavigate() + const location = useLocation() + const kbKey = location.state?.kbKey as string | undefined const [messages, setMessages] = useState([]) const [inputValue, setInputValue] = useState('') const [selectedModel, setSelectedModel] = useState( @@ -48,6 +50,19 @@ export function PlaygroundPage() { const currentKnowledgeBase = knowledgeBases?.find((kb) => kb.id === kbId) + useEffect(() => { + // Validate if kbKey is available + if (!kbKey && currentKnowledgeBase) { + const firstKey = currentKnowledgeBase.keys[0]?.kbKey + if (!firstKey) { + toast.error( + 'Nenhuma chave de API disponível para esta base de conhecimento' + ) + navigate('/bases') + } + } + }, [kbKey, currentKnowledgeBase, navigate]) + const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) } @@ -59,6 +74,14 @@ export function PlaygroundPage() { const handleSendMessage = async () => { if (!inputValue.trim() || !kbId) return + // Get the kbKey to use (from state or from current KB) + const keyToUse = kbKey || currentKnowledgeBase?.keys[0]?.kbKey + + if (!keyToUse) { + toast.error('Chave de API não disponível') + return + } + const userMessage: Message = { id: Date.now().toString(), content: inputValue, @@ -72,6 +95,7 @@ export function PlaygroundPage() { try { const response = await chatMutation.mutateAsync({ kbId, + kbKey: keyToUse, model: selectedModel, prompt: inputValue }) diff --git a/src/features/user-area/components/knowledge-base-detail-modal.tsx b/src/features/user-area/components/knowledge-base-detail-modal.tsx index 190f59a..cc6dd29 100644 --- a/src/features/user-area/components/knowledge-base-detail-modal.tsx +++ b/src/features/user-area/components/knowledge-base-detail-modal.tsx @@ -68,7 +68,12 @@ export function KnowledgeBaseDetailModal({ } const handleTestKnowledgeBase = () => { - navigate(`/playground/${knowledgeBase.id}`) + // Get the first available key + const firstKey = knowledgeBase.keys[0]?.kbKey + + navigate(`/playground/${knowledgeBase.id}`, { + state: { kbKey: firstKey } + }) onOpenChange(false) } From dc6cab8eef1c003de5ae5c9c0992856537413361 Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Sun, 19 Oct 2025 22:19:58 -0300 Subject: [PATCH 36/56] feat: enhance CreateBasePipeline to skip already completed steps and add retry functionality --- .../user-area/pages/create-base-pipeline.tsx | 194 ++++++++++++------ 1 file changed, 127 insertions(+), 67 deletions(-) diff --git a/src/features/user-area/pages/create-base-pipeline.tsx b/src/features/user-area/pages/create-base-pipeline.tsx index a832e08..a74fdab 100644 --- a/src/features/user-area/pages/create-base-pipeline.tsx +++ b/src/features/user-area/pages/create-base-pipeline.tsx @@ -146,81 +146,108 @@ export function CreateBasePipeline() { } try { - // Step 1: Create Knowledge Base - updateStepStatus(0, 'loading') - const createResult = await createKnowledgeBaseMutation.mutateAsync({ - name: formData.slug, - displayName: formData.name, - description: formData.description - }) + let kbId = pipeline.kbId + + // Step 1: Create Knowledge Base (skip if already created) + if (pipeline.steps[0].status !== 'success') { + updateStepStatus(0, 'loading') + const createResult = await createKnowledgeBaseMutation.mutateAsync({ + name: formData.slug, + displayName: formData.name, + description: formData.description + }) + + kbId = createResult.kb_id + setPipeline((prev) => ({ ...prev, kbId })) + updateStepStatus(0, 'success') + moveToNextStep() + } else { + // Already succeeded, just move forward + console.log('Step 1 já concluído, pulando...') + setPipeline((prev) => ({ + ...prev, + currentStep: Math.max(prev.currentStep, 1) + })) + } - setPipeline((prev) => ({ ...prev, kbId: createResult.kb_id })) - updateStepStatus(0, 'success') - moveToNextStep() + if (!kbId) { + throw new Error('KB ID não encontrado') + } - // Step 2: Upload Files - updateStepStatus(1, 'loading') + // Step 2: Upload Files (skip if already uploaded) + if (pipeline.steps[1].status !== 'success') { + updateStepStatus(1, 'loading') - const presignedResponse = await knowledgeBaseService.getUrlPresigned({ - bucketName, - kbId: createResult.kb_id - }) + const presignedResponse = await knowledgeBaseService.getUrlPresigned({ + bucketName, + kbId + }) - for (let i = 0; i < formData.files.length; i++) { - const file = formData.files[i] + for (let i = 0; i < formData.files.length; i++) { + const file = formData.files[i] - try { - // Upload file to S3 - await uploadFileToS3( - file, - presignedResponse.url, - presignedResponse.fields - ) - - // Update description to show progress - setPipeline((prev) => ({ - ...prev, - steps: prev.steps.map((step, index) => - index === 1 - ? { - ...step, - description: `Enviando arquivo ${i + 1} de ${ - formData.files.length - }: ${file.name}` - } - : step + try { + // Upload file to S3 + await uploadFileToS3( + file, + presignedResponse.url, + presignedResponse.fields ) - })) - } catch (error) { - console.error(`Error uploading file ${file.name}:`, error) - updateStepStatus(1, 'error', `Erro ao enviar arquivo: ${file.name}`) - return + + // Update description to show progress + setPipeline((prev) => ({ + ...prev, + steps: prev.steps.map((step, index) => + index === 1 + ? { + ...step, + description: `Enviando arquivo ${i + 1} de ${ + formData.files.length + }: ${file.name}` + } + : step + ) + })) + } catch (error) { + console.error(`Error uploading file ${file.name}:`, error) + updateStepStatus(1, 'error', `Erro ao enviar arquivo: ${file.name}`) + return + } } + + updateStepStatus(1, 'success') + setPipeline((prev) => ({ + ...prev, + steps: prev.steps.map((step, index) => + index === 1 + ? { + ...step, + description: `${formData.files.length} arquivos enviados com sucesso` + } + : step + ) + })) + moveToNextStep() + } else { + // Already succeeded, just move forward + console.log('Step 2 já concluído, pulando...') + setPipeline((prev) => ({ + ...prev, + currentStep: Math.max(prev.currentStep, 2) + })) } - updateStepStatus(1, 'success') - setPipeline((prev) => ({ - ...prev, - steps: prev.steps.map((step, index) => - index === 1 - ? { - ...step, - description: `${formData.files.length} arquivos enviados com sucesso` - } - : step - ) - })) - moveToNextStep() - - // Step 3: Sync Knowledge Base - updateStepStatus(2, 'loading') - await new Promise((resolve) => setTimeout(resolve, 5000)) // Sleep - await knowledgeBaseService.syncKnowledgeBase({ - bucketName, - kbId: createResult.kb_id - }) + // Step 3: Sync Knowledge Base (skip if already synced) + if (pipeline.steps[2].status !== 'success') { + updateStepStatus(2, 'loading') + await new Promise((resolve) => setTimeout(resolve, 5000)) // Sleep + await knowledgeBaseService.syncKnowledgeBase({ + bucketName, + kbId + }) - updateStepStatus(2, 'success') + updateStepStatus(2, 'success') + } // Success! toast.success('Base de conhecimento criada com sucesso!') @@ -237,7 +264,9 @@ export function CreateBasePipeline() { 'error', 'Erro inesperado durante o processo' ) - toast.error('Erro ao criar base de conhecimento') + toast.error( + 'Erro ao criar base de conhecimento: ' + (error as Error).message + ) } } @@ -252,6 +281,31 @@ export function CreateBasePipeline() { } }, []) + const handleRetry = () => { + // Reset error states but keep success states + setPipeline((prev) => ({ + ...prev, + steps: prev.steps.map((step) => + step.status === 'error' + ? { ...step, status: 'pending', errorMessage: undefined } + : step + ) + })) + + // Show info about resuming + const successCount = pipeline.steps.filter( + (s) => s.status === 'success' + ).length + if (successCount > 0) { + toast.success( + `Retomando do ponto de falha. ${successCount} etapa(s) já concluída(s) serão puladas.` + ) + } + + // Execute steps again (will skip already successful steps) + executeSteps() + } + const getStepIcon = (step: PipelineStep) => { switch (step.status) { case 'loading': @@ -388,10 +442,16 @@ export function CreateBasePipeline() { - +
)} From 8a7027f903dd6355476aa6c0e09c23932fde3fa4 Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Sun, 19 Oct 2025 22:20:03 -0300 Subject: [PATCH 37/56] fix: remove console log for user data fetching --- src/services/user.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/user.ts b/src/services/user.ts index ad77f0f..b029f79 100644 --- a/src/services/user.ts +++ b/src/services/user.ts @@ -92,7 +92,6 @@ export const userService = { headers: getAuthHeader() }) const data = response.data.user - console.log('User data fetched:', data) return { id: data.user_id, name: data.name, From f8f3f49d2f27f182ee8f99f3db2d320fa139a4c0 Mon Sep 17 00:00:00 2001 From: Enzo Sakamoto Date: Sun, 19 Oct 2025 22:41:49 -0300 Subject: [PATCH 38/56] feat: add "Remember Email" checkbox functionality to login form --- src/features/login/pages/login.tsx | 46 ++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/src/features/login/pages/login.tsx b/src/features/login/pages/login.tsx index 5ee33b5..4bc8126 100644 --- a/src/features/login/pages/login.tsx +++ b/src/features/login/pages/login.tsx @@ -2,7 +2,7 @@ import { Background } from '@/shared/components/background' import { Layout } from '@/shared/components/layout' import { motion } from 'framer-motion' import { Button } from '@/shared/components/button' -import { useState } from 'react' +import { useState, useEffect } from 'react' import { Eye, EyeOff } from 'lucide-react' import { BackgroundBlobs } from '@/shared/components/background-blobs' import { @@ -28,25 +28,49 @@ import { Form } from '@/shared/components/form' import { Input } from '@/shared/components/input' +import { Checkbox } from '@/shared/components/checkbox' import { Link, useNavigate } from 'react-router-dom' import { containerVariants, cardVariants } from '@/shared/utils/animations' import { useLoginUserMutation } from '@/shared/hooks/use-user' import toast from 'react-hot-toast' import { AxiosError } from 'axios' +const REMEMBER_EMAIL_KEY = 'knowly_remember_email' + export function LoginPage() { const [showPassword, setShowPassword] = useState(false) + const [rememberEmail, setRememberEmail] = useState(false) const { mutateAsync: loginUser, isPending } = useLoginUserMutation() const navigate = useNavigate() + // Get saved email before initializing form + const savedEmail = localStorage.getItem(REMEMBER_EMAIL_KEY) + const form = useForm({ resolver: zodResolver(loginFormSchema), - defaultValues: loginFormInitialValues, + defaultValues: { + ...loginFormInitialValues, + email: savedEmail || loginFormInitialValues.email + }, mode: 'onBlur' }) + // Set checkbox state on mount + useEffect(() => { + if (savedEmail) { + setRememberEmail(true) + } + }, [savedEmail]) + const onSubmit = async (values: LoginFormData) => { try { + // Save or remove email from localStorage based on checkbox + if (rememberEmail) { + localStorage.setItem(REMEMBER_EMAIL_KEY, values.email) + } else { + localStorage.removeItem(REMEMBER_EMAIL_KEY) + } + const response = await loginUser(values) localStorage.setItem('token', response.id_token) toast.success('Login realizado com sucesso!') @@ -133,6 +157,24 @@ export function LoginPage() { )} /> + + {/* Remember Email Checkbox */} +
+ + setRememberEmail(checked as boolean) + } + /> + +
+ + + + {!isCollapsed && ( + +

+ Documentação +

+
+ )} + + + {docsItems.map((item) => { + const isActive = location.pathname === item.path + return ( + + + {item.icon} + {!isCollapsed && {item.label}} + + + ) + })} + +
+ + ) +} diff --git a/src/shared/components/typography.tsx b/src/shared/components/typography.tsx new file mode 100644 index 0000000..9ef1961 --- /dev/null +++ b/src/shared/components/typography.tsx @@ -0,0 +1,96 @@ +import { ReactNode } from 'react' +import { cn } from '@/shared/utils/cn' + +interface TypographyProps { + children: ReactNode + className?: string +} + +export function h1({ children, className }: TypographyProps) { + return ( +

+ {children} +

+ ) +} + +export function h2({ children, className }: TypographyProps) { + return ( +

+ {children} +

+ ) +} + +export function h3({ children, className }: TypographyProps) { + return ( +

+ {children} +

+ ) +} + +export function p({ children, className }: TypographyProps) { + return ( +

+ {children} +

+ ) +} + +export function ul({ children, className }: TypographyProps) { + return ( +
    + {children} +
+ ) +} + +export function li({ children, className }: TypographyProps) { + return ( +
  • + {children} +
  • + ) +} + +export function pre({ children, className }: TypographyProps) { + return ( +
    +      {children}
    +    
    + ) +} + +export function code({ children, className }: TypographyProps) { + return ( + + {children} + + ) +} From 4568c89bd1b9a13df6f6cd0df054bb41e9122007 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 01:15:53 +0000 Subject: [PATCH 49/56] Enhance documentation styling with modern design patterns Co-authored-by: enzosakamoto <98707474+enzosakamoto@users.noreply.github.com> --- src/features/docs/pages/about-project.tsx | 5 ++- src/features/docs/pages/foundation-models.tsx | 5 ++- src/features/docs/pages/integration.tsx | 5 ++- src/features/docs/pages/knowledge-bases.tsx | 5 ++- .../docs/pages/playground-testing.tsx | 5 ++- .../docs/pages/subscription-management.tsx | 5 ++- src/shared/components/docs-layout.tsx | 8 +++-- src/shared/components/typography.tsx | 33 +++++++++++++++---- 8 files changed, 43 insertions(+), 28 deletions(-) diff --git a/src/features/docs/pages/about-project.tsx b/src/features/docs/pages/about-project.tsx index a488707..6e49858 100644 --- a/src/features/docs/pages/about-project.tsx +++ b/src/features/docs/pages/about-project.tsx @@ -3,11 +3,10 @@ import { h1, h2, p } from '@/shared/components/typography' export function AboutProjectPage() { return ( - {h1({ children: 'Sobre o Projeto' })} @@ -51,7 +50,7 @@ export function AboutProjectPage() { ) })} - + ) } diff --git a/src/features/docs/pages/foundation-models.tsx b/src/features/docs/pages/foundation-models.tsx index 32d39d6..407c874 100644 --- a/src/features/docs/pages/foundation-models.tsx +++ b/src/features/docs/pages/foundation-models.tsx @@ -3,11 +3,10 @@ import { h1, h2, p, ul, li } from '@/shared/components/typography' export function FoundationModelsPage() { return ( - {h1({ children: 'Modelos de Fundação AWS' })} @@ -123,7 +122,7 @@ export function FoundationModelsPage() { ) })} - + ) } diff --git a/src/features/docs/pages/integration.tsx b/src/features/docs/pages/integration.tsx index e1c3c9e..10a5a86 100644 --- a/src/features/docs/pages/integration.tsx +++ b/src/features/docs/pages/integration.tsx @@ -3,11 +3,10 @@ import { h1, h2, p, ul, li, pre } from '@/shared/components/typography' export function IntegrationPage() { return ( - {h1({ children: 'Como integrar com sua base de conhecimento' })} @@ -77,7 +76,7 @@ Body: ) })} - + ) } diff --git a/src/features/docs/pages/knowledge-bases.tsx b/src/features/docs/pages/knowledge-bases.tsx index 798803d..78192e0 100644 --- a/src/features/docs/pages/knowledge-bases.tsx +++ b/src/features/docs/pages/knowledge-bases.tsx @@ -3,11 +3,10 @@ import { h1, h2, p, ul, li } from '@/shared/components/typography' export function KnowledgeBasesPage() { return ( - {h1({ children: 'Gerenciando Bases de Conhecimento' })} @@ -57,7 +56,7 @@ export function KnowledgeBasesPage() { ) })} - + ) } diff --git a/src/features/docs/pages/playground-testing.tsx b/src/features/docs/pages/playground-testing.tsx index 70aa9c0..1e7a64e 100644 --- a/src/features/docs/pages/playground-testing.tsx +++ b/src/features/docs/pages/playground-testing.tsx @@ -3,11 +3,10 @@ import { h1, h2, p, ul, li } from '@/shared/components/typography' export function PlaygroundTestingPage() { return ( - {h1({ children: 'Testando sua Base de Conhecimento no Playground' })} @@ -62,7 +61,7 @@ export function PlaygroundTestingPage() { ) })} - + ) } diff --git a/src/features/docs/pages/subscription-management.tsx b/src/features/docs/pages/subscription-management.tsx index 7502526..a6f0251 100644 --- a/src/features/docs/pages/subscription-management.tsx +++ b/src/features/docs/pages/subscription-management.tsx @@ -3,11 +3,10 @@ import { h1, h2, p, ul, li } from '@/shared/components/typography' export function SubscriptionManagementPage() { return ( - {h1({ children: 'Gerenciando seu Plano de Assinatura' })} @@ -60,7 +59,7 @@ export function SubscriptionManagementPage() { ) })} - + ) } diff --git a/src/shared/components/docs-layout.tsx b/src/shared/components/docs-layout.tsx index 2a6c774..709d8f4 100644 --- a/src/shared/components/docs-layout.tsx +++ b/src/shared/components/docs-layout.tsx @@ -13,9 +13,11 @@ export function DocsLayout() { -
    -
    - +
    +
    +
    + +
    diff --git a/src/shared/components/typography.tsx b/src/shared/components/typography.tsx index 9ef1961..5de1bf1 100644 --- a/src/shared/components/typography.tsx +++ b/src/shared/components/typography.tsx @@ -10,7 +10,8 @@ export function h1({ children, className }: TypographyProps) { return (

    @@ -23,7 +24,7 @@ export function h2({ children, className }: TypographyProps) { return (

    @@ -47,7 +48,12 @@ export function h3({ children, className }: TypographyProps) { export function p({ children, className }: TypographyProps) { return ( -

    +

    {children}

    ) @@ -55,7 +61,7 @@ export function p({ children, className }: TypographyProps) { export function ul({ children, className }: TypographyProps) { return ( -