Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"author": "",
"private": true,
"license": "UNLICENSED",
"packageManager": "pnpm@9.1.2",
"packageManager": "pnpm@9.1.0",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
Expand Down
3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-scroll-area": "^1.2.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@types/dom-speech-recognition": "^0.0.4",
Expand Down
Binary file added frontend/public/codefox.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
120 changes: 120 additions & 0 deletions frontend/public/codefox.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed frontend/public/ollama.png
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use client';

import React, { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
Expand All @@ -18,6 +19,12 @@ const LoginPage = () => {
password: '',
});
const [error, setError] = useState<string | null>(null);
const [mounted, setMounted] = useState(false);
const router = useRouter();

useEffect(() => {
setMounted(true);
}, []);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
Expand All @@ -27,7 +34,6 @@ const LoginPage = () => {
}));
};

const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
Expand Down Expand Up @@ -57,16 +63,21 @@ const LoginPage = () => {

return (
<div className="flex items-center justify-center min-h-screen bg-light-background dark:bg-dark-background">
<div className="w-full max-w-md px-8">
<div
className={`w-full max-w-md px-8 transition-all duration-1000 ease-in-out
${mounted ? 'translate-y-0 opacity-100' : 'translate-y-4 opacity-0'}`}
>
<div className="space-y-2 mb-8">
<h1 className="text-2xl font-semibold">Sign In</h1>
<h1 className="text-2xl font-semibold text-light-text-primary dark:text-dark-text-primary">
Sign In
</h1>
<p className="text-gray-500">
Enter credentials to login to your account
</p>
</div>

{error && (
<div className="mb-4 p-3 text-sm text-red-500 bg-red-50 rounded-lg">
<div className="mb-4 p-3 text-sm text-red-500 bg-red-50 dark:bg-red-900/20 rounded-lg">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add aria-live for better accessibility

Error messages should be announced by screen readers when they appear.

-          <div className="mb-4 p-3 text-sm text-red-500 bg-red-50 dark:bg-red-900/20 rounded-lg">
+          <div className="mb-4 p-3 text-sm text-red-500 bg-red-50 dark:bg-red-900/20 rounded-lg" role="alert" aria-live="polite">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="mb-4 p-3 text-sm text-red-500 bg-red-50 dark:bg-red-900/20 rounded-lg">
<div className="mb-4 p-3 text-sm text-red-500 bg-red-50 dark:bg-red-900/20 rounded-lg" role="alert" aria-live="polite">

{error}
</div>
)}
Expand All @@ -83,7 +94,10 @@ const LoginPage = () => {
value={formData.username}
onChange={handleChange}
required
className="h-12 rounded-lg border-gray-200 focus:border-gray-300 focus:ring-0"
className="h-12 rounded-lg border-light-border dark:border-dark-border
bg-light-surface dark:bg-dark-surface
text-light-text-primary dark:text-dark-text-primary
focus:outline-none focus:ring-2 focus:ring-primary-400 dark:focus:ring-primary-500 focus:border-transparent"
placeholder="Enter your username"
disabled={isLoading}
/>
Expand All @@ -100,15 +114,19 @@ const LoginPage = () => {
value={formData.password}
onChange={handleChange}
required
className="h-12 rounded-lg border-gray-200 focus:border-gray-300 focus:ring-0"
className="h-12 rounded-lg border-light-border dark:border-dark-border
bg-light-surface dark:bg-dark-surface
text-light-text-primary dark:text-dark-text-primary
focus:outline-none focus:ring-2 focus:ring-primary-400 dark:focus:ring-primary-500 focus:border-transparent"
placeholder="Enter your password"
disabled={isLoading}
/>
</div>

<Button
type="submit"
className="w-full h-12 bg-primary-500 hover:bg-primary-600 text-white rounded-lg font-medium"
className="w-full h-12 bg-primary-500 hover:bg-primary-600 dark:bg-primary-600 dark:hover:bg-primary-700
text-white rounded-lg font-medium"
Comment on lines +128 to +129
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add focus-visible styles for keyboard navigation

The button should have visible focus indicators for keyboard users.

-            className="w-full h-12 bg-primary-500 hover:bg-primary-600 dark:bg-primary-600 dark:hover:bg-primary-700 
-                     text-white rounded-lg font-medium"
+            className="w-full h-12 bg-primary-500 hover:bg-primary-600 dark:bg-primary-600 dark:hover:bg-primary-700 
+                     text-white rounded-lg font-medium focus-visible:ring-2 focus-visible:ring-offset-2 
+                     focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
className="w-full h-12 bg-primary-500 hover:bg-primary-600 dark:bg-primary-600 dark:hover:bg-primary-700
text-white rounded-lg font-medium"
className="w-full h-12 bg-primary-500 hover:bg-primary-600 dark:bg-primary-600 dark:hover:bg-primary-700
text-white rounded-lg font-medium focus-visible:ring-2 focus-visible:ring-offset-2
focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400"

disabled={isLoading}
>
{isLoading ? 'Signing In...' : 'Sign In'}
Expand Down
98 changes: 98 additions & 0 deletions frontend/src/app/(main)/MainLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
'use client';

import React, { useEffect, useState } from 'react';
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '@/components/ui/resizable';
import { cn } from '@/lib/utils';
import { usePathname } from 'next/navigation';
import Sidebar from '@/components/sidebar';
import { useChatList } from '../hooks/useChatList';

export default function MainLayout({
children,
}: {
children: React.ReactNode;
}) {
const [isCollapsed, setIsCollapsed] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const defaultLayout = [30, 160];
const navCollapsedSize = 10;

const pathname = usePathname();
const currentChatId = pathname.split('/')[1] || '';

const {
chats,
loading,
error,
chatListUpdated,
setChatListUpdated,
refetchChats,
} = useChatList();
Comment on lines +24 to +34
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add path validation and error handling.

The current pathname splitting implementation could be unsafe if the path structure changes.

Consider applying this safer approach:

-  const currentChatId = pathname.split('/')[1] || '';
+  const currentChatId = (() => {
+    const parts = pathname.split('/');
+    if (parts.length < 2) return '';
+    const id = parts[1];
+    return /^[a-zA-Z0-9-_]+$/.test(id) ? id : '';
+  })();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const pathname = usePathname();
const currentChatId = pathname.split('/')[1] || '';
const {
chats,
loading,
error,
chatListUpdated,
setChatListUpdated,
refetchChats,
} = useChatList();
const pathname = usePathname();
const currentChatId = (() => {
const parts = pathname.split('/');
if (parts.length < 2) return '';
const id = parts[1];
return /^[a-zA-Z0-9-_]+$/.test(id) ? id : '';
})();
const {
chats,
loading,
error,
chatListUpdated,
setChatListUpdated,
refetchChats,
} = useChatList();


useEffect(() => {
const checkScreenWidth = () => {
setIsMobile(window.innerWidth <= 1023);
};
checkScreenWidth();
window.addEventListener('resize', checkScreenWidth);
return () => {
window.removeEventListener('resize', checkScreenWidth);
};
}, []);
Comment on lines +36 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Optimize resize handling and align breakpoints with Tailwind.

The resize handler could benefit from debouncing, and the breakpoint should match Tailwind's lg breakpoint (1024px).

Consider applying these improvements:

+import { debounce } from 'lodash';
+
 useEffect(() => {
-  const checkScreenWidth = () => {
+  const checkScreenWidth = debounce(() => {
     setIsMobile(window.innerWidth <= 1023);
-  };
+  }, 250);
   checkScreenWidth();
   window.addEventListener('resize', checkScreenWidth);
   return () => {
     window.removeEventListener('resize', checkScreenWidth);
+    checkScreenWidth.cancel();
   };
 }, []);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const checkScreenWidth = () => {
setIsMobile(window.innerWidth <= 1023);
};
checkScreenWidth();
window.addEventListener('resize', checkScreenWidth);
return () => {
window.removeEventListener('resize', checkScreenWidth);
};
}, []);
import { debounce } from 'lodash';
useEffect(() => {
const checkScreenWidth = debounce(() => {
setIsMobile(window.innerWidth <= 1023);
}, 250);
checkScreenWidth();
window.addEventListener('resize', checkScreenWidth);
return () => {
window.removeEventListener('resize', checkScreenWidth);
checkScreenWidth.cancel();
};
}, []);


return (
<main className="flex h-[calc(100dvh)] flex-col items-center">
<ResizablePanelGroup
direction="horizontal"
onLayout={(sizes: number[]) => {
document.cookie = `react-resizable-panels:layout=${JSON.stringify(sizes)}`;
}}
className="h-screen items-stretch"
>
<ResizablePanel
defaultSize={defaultLayout[0]}
collapsedSize={navCollapsedSize}
collapsible={true}
minSize={isMobile ? 0 : 12}
maxSize={isMobile ? 0 : 16}
onCollapse={() => {
setIsCollapsed(true);
document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(true)}`;
}}
onExpand={() => {
setIsCollapsed(false);
document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(false)}`;
}}
className={cn(
isCollapsed
? 'min-w-[50px] md:min-w-[70px] transition-all duration-300 ease-in-out'
: 'hidden md:block'
)}
>
<Sidebar
isCollapsed={isCollapsed}
isMobile={isMobile}
currentChatId={currentChatId}
chatListUpdated={chatListUpdated}
setChatListUpdated={setChatListUpdated}
chats={chats}
loading={loading}
error={error}
onRefetch={refetchChats}
/>
</ResizablePanel>
<ResizableHandle className={cn('hidden md:flex')} withHandle />
<ResizablePanel
className="h-full w-full flex justify-center"
defaultSize={defaultLayout[1]}
>
{children}
</ResizablePanel>
</ResizablePanelGroup>
</main>
);
}
Comment on lines +47 to +98
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance maintainability and accessibility of the layout.

Consider the following improvements:

  1. Extract cookie handling logic to a custom hook
  2. Add accessibility attributes to resizable panels
  3. Simplify className logic

Here's a suggested implementation for the cookie handling hook:

// hooks/useLayoutPreferences.ts
export const useLayoutPreferences = () => {
  const [layout, setLayout] = useState<number[]>([30, 160]);
  const [isCollapsed, setIsCollapsed] = useState(false);

  const updateLayout = (sizes: number[]) => {
    setLayout(sizes);
    document.cookie = `react-resizable-panels:layout=${JSON.stringify(sizes)}`;
  };

  const updateCollapsed = (collapsed: boolean) => {
    setIsCollapsed(collapsed);
    document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(collapsed)}`;
  };

  return { layout, isCollapsed, updateLayout, updateCollapsed };
};

And update the ResizablePanel with accessibility attributes:

 <ResizablePanel
+  role="complementary"
+  aria-label="Sidebar navigation"
   defaultSize={defaultLayout[0]}
   collapsedSize={navCollapsedSize}
   collapsible={true}

63 changes: 63 additions & 0 deletions frontend/src/app/(main)/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
'use client';

import { useEffect, useRef, useState } from 'react';
import { useParams } from 'next/navigation';
import { Message } from '@/components/types';
import { useModels } from '@/app/hooks/useModels';
import ChatContent from '@/components/chat/chat';
import { useChatStream } from '../../hooks/useChatStream';
import { useQuery } from '@apollo/client';
import { GET_CHAT_HISTORY } from '@/graphql/request';
import { toast } from 'sonner';

export default function ChatPage() {
const params = useParams();
const chatId = params.id as string;

Comment on lines +14 to +16
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add validation for chatId parameter.

The chatId is directly used from URL params without validation. Consider adding validation to handle invalid or missing chatId cases.

 const params = useParams();
-const chatId = params.id as string;
+const chatId = params.id;
+if (!chatId || typeof chatId !== 'string') {
+  throw new Error('Invalid chat ID');
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const params = useParams();
const chatId = params.id as string;
const params = useParams();
const chatId = params.id;
if (!chatId || typeof chatId !== 'string') {
throw new Error('Invalid chat ID');
}

// Core message states
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const formRef = useRef<HTMLFormElement>(null);

const { models } = useModels();
const [selectedModel, setSelectedModel] = useState<string>(
models[0] || 'Loading models'
);
Comment on lines +22 to +25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle empty models array case.

The current initialization of selectedModel could result in 'Loading models' being used as an actual model identifier.

-const [selectedModel, setSelectedModel] = useState<string>(
-  models[0] || 'Loading models'
-);
+const [selectedModel, setSelectedModel] = useState<string | null>(null);
+
+useEffect(() => {
+  if (models.length > 0) {
+    setSelectedModel(models[0]);
+  }
+}, [models]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { models } = useModels();
const [selectedModel, setSelectedModel] = useState<string>(
models[0] || 'Loading models'
);
const { models } = useModels();
const [selectedModel, setSelectedModel] = useState<string | null>(null);
useEffect(() => {
if (models.length > 0) {
setSelectedModel(models[0]);
}
}, [models]);


useQuery(GET_CHAT_HISTORY, {
variables: { chatId: params.id },
onCompleted: (data) => {
if (data?.getChatHistory) {
setMessages(data.getChatHistory);
}
},
onError: (error) => {
toast.error('Failed to load chat history');
},
});

const { loadingSubmit, handleSubmit, handleInputChange, stop } =
useChatStream({
chatId,
input,
setInput,
setMessages,
selectedModel,
});

return (
<ChatContent
chatId={chatId}
setSelectedModel={setSelectedModel}
messages={messages}
input={input}
handleInputChange={handleInputChange}
handleSubmit={handleSubmit}
loadingSubmit={loadingSubmit}
stop={stop}
formRef={formRef}
setInput={setInput}
setMessages={setMessages}
/>
);
}
Loading