From c45dcfaa522410101b6850e5e777b09d70dffcaa Mon Sep 17 00:00:00 2001 From: nick134 <76399455+nick134-bit@users.noreply.github.com> Date: Sat, 3 May 2025 18:33:46 +0200 Subject: [PATCH 1/3] refactor: general structure, code duplication refactor: Update ModelDetailPage to use promise parameters and add ESLint comments for type assertions refactor: Remove unnecessary imports and add ESLint comments for type assertions feat: Add ESLint configuration and ignore files chore: remove log Co-Authored-By: zJ <19760191+zJuuu@users.noreply.github.com> --- .eslintignore | 17 + .eslintrc.json | 63 +++ app/api/auth/session/refresh/route.ts | 9 +- app/api/auth/session/route.ts | 8 +- app/api/auth/status/route.ts | 1 + app/api/chat/route.ts | 15 +- app/api/health/route.ts | 1 + app/api/models/route.ts | 3 +- app/api/transcription/route.ts | 1 + .../use-chat-streaming-tool-calls/route.ts | 1 + app/api/use-chat-vision/route.ts | 1 + app/config/models.ts | 2 +- app/context/ChatContext.tsx | 110 +++- app/layout.tsx | 3 +- app/models/[modelId]/chat/layout.tsx | 7 +- app/models/[modelId]/chat/metadata.tsx | 1 + app/models/[modelId]/chat/page.tsx | 166 +------ app/models/[modelId]/not-found.tsx | 1 + app/models/[modelId]/page.tsx | 3 +- app/models/page.tsx | 1 + app/page.tsx | 3 +- app/privacy/page.tsx | 2 +- app/sitemap.ts | 1 + app/terms/page.tsx | 2 +- components/branding/akash-chat-logo.tsx | 1 + components/branding/akash-gen.tsx | 1 + components/branding/akash-sign-logo.tsx | 1 + components/branding/deepseek.tsx | 3 +- components/branding/llama-1.tsx | 3 +- components/branding/llama-2.tsx | 1 + components/branding/llama-3.tsx | 3 +- components/branding/llama-4.tsx | 1 + components/branding/model-thumbnail.tsx | 3 +- components/chat-home.tsx | 249 ---------- components/{ => chat}/chat-header.tsx | 12 +- components/chat/chat-home.tsx | 126 +++++ components/{ => chat}/chat-input.tsx | 98 +--- .../{ChatContent.tsx => chat-messages.tsx} | 52 +- components/{ => chat}/chat-sidebar.tsx | 36 +- components/code-block.tsx | 8 +- components/draggable-chat-item.tsx | 7 +- components/droppable-folder.tsx | 5 +- components/file-context.tsx | 15 +- components/file-upload.tsx | 6 +- components/layout/MainLayout.tsx | 23 +- components/markdown.tsx | 5 +- components/message.tsx | 102 +--- components/model-config.tsx | 11 +- components/models/model-detail-client.tsx | 17 +- components/models/models-layout-client.tsx | 2 +- components/models/models-page-client.tsx | 13 +- components/theme-toggle.tsx | 2 +- components/ui/button.tsx | 2 +- components/ui/dialog.tsx | 2 +- components/ui/dropdown-menu.tsx | 2 +- components/ui/separator.tsx | 2 +- components/ui/sheet.tsx | 2 +- components/ui/sidebar.tsx | 8 +- components/ui/slider.tsx | 2 +- components/ui/switch.tsx | 2 +- components/ui/tooltip.tsx | 2 +- hooks/use-websocket-transcription.ts | 2 +- lib/auth.ts | 9 +- lib/migrate-old-chats.ts | 9 - lib/models.ts | 3 +- lib/redis.ts | 3 +- lib/tools.ts | 1 + lib/utils.ts | 6 +- lib/websocket-transcription-client.ts | 32 +- package-lock.json | 470 +++++++++++++++--- package.json | 8 +- utils/file-handler.ts | 68 +++ utils/message-parser.ts | 77 +++ 73 files changed, 1109 insertions(+), 831 deletions(-) create mode 100644 .eslintignore create mode 100644 .eslintrc.json delete mode 100644 components/chat-home.tsx rename components/{ => chat}/chat-header.tsx (98%) create mode 100644 components/chat/chat-home.tsx rename components/{ => chat}/chat-input.tsx (82%) rename components/chat/{ChatContent.tsx => chat-messages.tsx} (89%) rename components/{ => chat}/chat-sidebar.tsx (97%) create mode 100644 utils/file-handler.ts create mode 100644 utils/message-parser.ts diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..c32ad98 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,17 @@ +.next +node_modules +out +public +*.d.ts +*.js.map +*.css +*.svg +*.png +*.jpg +*.jpeg +*.gif +*.ico +*.woff +*.woff2 +*.ttf +*.eot \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..82d01d8 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,63 @@ +{ + "extends": [ + "next/core-web-vitals", + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react/recommended" + ], + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint", + "react", + "import" + ], + "rules": { + "@typescript-eslint/no-unused-vars": ["error", { + "argsIgnorePattern": "^_|props|node", + "varsIgnorePattern": "^_", + "ignoreRestSiblings": true + }], + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/consistent-type-definitions": ["error", "interface"], + "@typescript-eslint/no-non-null-assertion": "warn", + "react/prop-types": "off", + "react/react-in-jsx-scope": "off", + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", + "react/no-unescaped-entities": "warn", + "react/no-unused-prop-types": "warn", + "react/jsx-no-duplicate-props": "error", + "react/jsx-key": ["error", { "checkFragmentShorthand": true }], + "no-console": ["warn", { "allow": ["warn", "error", "info"] }], + "prefer-const": "error", + "no-var": "error", + "no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 0 }], + "eqeqeq": ["error", "always"], + "curly": ["error", "all"], + "no-unused-expressions": "error", + "import/no-unresolved": "off", + "import/order": ["error", { + "groups": ["builtin", "external", "internal", "parent", "sibling", "index"], + "newlines-between": "always", + "alphabetize": { "order": "asc" } + }], + "@typescript-eslint/ban-types": "warn", + "@typescript-eslint/ban-ts-comment": ["warn", { "minimumDescriptionLength": 3 }], + "@next/next/no-img-element": "warn", + "no-empty": "warn", + "@typescript-eslint/no-var-requires": "warn", + "no-empty-pattern": "warn" + }, + "settings": { + "react": { + "version": "detect" + } + }, + "ignorePatterns": [ + "node_modules/", + ".next/", + "out/", + "public/", + "*.config.js" + ] +} \ No newline at end of file diff --git a/app/api/auth/session/refresh/route.ts b/app/api/auth/session/refresh/route.ts index 2f4df2d..7dcde9e 100644 --- a/app/api/auth/session/refresh/route.ts +++ b/app/api/auth/session/refresh/route.ts @@ -1,9 +1,11 @@ +import crypto from 'crypto'; + import { NextResponse } from 'next/server'; + +import { CACHE_TTL } from '@/app/config/api'; +import { checkApiAccessToken } from '@/lib/auth'; import { validateSession, storeSession } from '@/lib/redis'; -import crypto from 'crypto'; import redis from '@/lib/redis'; -import { ACCESS_TOKEN, CACHE_TTL } from '@/app/config/api'; -import { checkApiAccessToken } from '@/lib/auth'; const RATE_LIMIT_WINDOW = Math.floor(CACHE_TTL * 0.20 * 0.25); const MAX_REQUESTS = 3; @@ -56,7 +58,6 @@ export async function POST(request: Request) { return authCheckResponse; } - if (await isRateLimited(currentToken)) { return NextResponse.json( { error: 'Too many refresh attempts. Please try again later.' }, diff --git a/app/api/auth/session/route.ts b/app/api/auth/session/route.ts index 6adb374..dc7deda 100644 --- a/app/api/auth/session/route.ts +++ b/app/api/auth/session/route.ts @@ -1,8 +1,10 @@ -import { NextResponse } from 'next/server'; import crypto from 'crypto'; -import { storeSession } from '@/lib/redis'; -import { ACCESS_TOKEN, CACHE_TTL } from '@/app/config/api'; + +import { NextResponse } from 'next/server'; + +import { CACHE_TTL } from '@/app/config/api'; import { checkApiAccessToken } from '@/lib/auth'; +import { storeSession } from '@/lib/redis'; export async function GET(request: Request) { if (process.env.NODE_ENV === 'production') { diff --git a/app/api/auth/status/route.ts b/app/api/auth/status/route.ts index 278d5e8..1855034 100644 --- a/app/api/auth/status/route.ts +++ b/app/api/auth/status/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from 'next/server'; + import { ACCESS_TOKEN } from '@/app/config/api'; /** diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index d4cf193..0f85ba0 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,13 +1,14 @@ import { createOpenAI } from '@ai-sdk/openai'; import { streamText, createDataStreamResponse, generateText, simulateReadableStream, Message } from 'ai'; + import { apiEndpoint, apiKey, imgGenFnModel, DEFAULT_SYSTEM_PROMPT } from '@/app/config/api'; import { defaultModel, models } from '@/app/config/models'; -import { generateImageTool } from '@/lib/tools'; -import { getAvailableModels } from '@/lib/models'; import { withAuth } from '@/lib/auth'; +import { getAvailableModels } from '@/lib/models'; +import { generateImageTool } from '@/lib/tools'; -const { Tiktoken } = require("tiktoken/lite"); const cl100k_base = require("tiktoken/encoders/cl100k_base.json"); +const { Tiktoken } = require("tiktoken/lite"); // Allow streaming responses up to 30 seconds export const maxDuration = 30; @@ -26,12 +27,13 @@ const openai = createOpenAI({ // Define the handler function to be wrapped with authentication async function handlePostRequest(req: Request) { // Extract the `messages` and `model` from the body of the request - let { messages, model, system, temperature, topP, context } = await req.json(); + const body = await req.json(); + const { messages, system, temperature, topP, context } = body; + let { model } = body; // Get available models from cache or API const allModels = await getAvailableModels(); const selectedModel = allModels.find(m => m.id === model) || models.find(m => m.id === defaultModel); - const encoding = new Tiktoken( cl100k_base.bpe_ranks, cl100k_base.special_tokens, @@ -130,7 +132,6 @@ async function handlePostRequest(req: Request) { } } - return createDataStreamResponse({ execute: dataStream => { const result = streamText({ @@ -167,5 +168,3 @@ async function handlePostRequest(req: Request) { // Export the wrapped handler export const POST = withAuth(handlePostRequest); - - diff --git a/app/api/health/route.ts b/app/api/health/route.ts index 5bf7fbe..16895e8 100644 --- a/app/api/health/route.ts +++ b/app/api/health/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from 'next/server'; + import { apiEndpoint, apiKey, imgEndpoint, imgApiKey } from '@/app/config/api'; import { models } from '@/app/config/models'; diff --git a/app/api/models/route.ts b/app/api/models/route.ts index f011454..383a94a 100644 --- a/app/api/models/route.ts +++ b/app/api/models/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from 'next/server'; -import { getAvailableModels } from '@/lib/models'; + import { withAuth } from '@/lib/auth'; +import { getAvailableModels } from '@/lib/models'; async function handleGetRequest() { const models = await getAvailableModels(); diff --git a/app/api/transcription/route.ts b/app/api/transcription/route.ts index a3ab4a1..83b391f 100644 --- a/app/api/transcription/route.ts +++ b/app/api/transcription/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from 'next/server'; + import { WS_TRANSCRIPTION_URLS, WS_TRANSCRIPTION_MODEL } from '@/app/config/api'; import { withAuth } from '@/lib/auth'; diff --git a/app/api/use-chat-streaming-tool-calls/route.ts b/app/api/use-chat-streaming-tool-calls/route.ts index 6331571..04f46f2 100644 --- a/app/api/use-chat-streaming-tool-calls/route.ts +++ b/app/api/use-chat-streaming-tool-calls/route.ts @@ -1,6 +1,7 @@ import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; import { z } from 'zod'; + import { withAuth } from '@/lib/auth'; // Allow streaming responses up to 30 seconds diff --git a/app/api/use-chat-vision/route.ts b/app/api/use-chat-vision/route.ts index 223822b..887dbe2 100644 --- a/app/api/use-chat-vision/route.ts +++ b/app/api/use-chat-vision/route.ts @@ -1,5 +1,6 @@ import { openai } from '@ai-sdk/openai'; import { streamText } from 'ai'; + import { withAuth } from '@/lib/auth'; // Allow streaming responses up to 30 seconds diff --git a/app/config/models.ts b/app/config/models.ts index 13c9d49..a2740b8 100644 --- a/app/config/models.ts +++ b/app/config/models.ts @@ -16,7 +16,7 @@ export interface Model { deployUrl?: string; } -export let models: Model[] = [ +export const models: Model[] = [ { id: 'Qwen3-235B-A22B-FP8', name: 'Qwen3 235B A22B', diff --git a/app/context/ChatContext.tsx b/app/context/ChatContext.tsx index 55701b5..5fc4a12 100644 --- a/app/context/ChatContext.tsx +++ b/app/context/ChatContext.tsx @@ -1,18 +1,21 @@ 'use client'; -import React, { createContext, useContext, useState, useEffect } from 'react'; -import { useChat } from 'ai/react'; import type { Message as AIMessage } from 'ai'; +import { useChat } from 'ai/react'; +import React, { createContext, useContext, useState, useEffect } from 'react'; import { useWindowSize } from 'usehooks-ts'; + +import { DEFAULT_SYSTEM_PROMPT } from '@/app/config/api'; +import { CACHE_TTL } from '@/app/config/api'; import { models as defaultModels, defaultModel, Model } from '@/app/config/models'; +import { ChatHistory } from '@/components/chat/chat-sidebar'; import { useChatHistory } from '@/hooks/use-chat-history'; -import { useFolders } from '@/hooks/use-folders'; -import { DEFAULT_SYSTEM_PROMPT } from '@/app/config/api'; +import { Folder, useFolders } from '@/hooks/use-folders'; import { getAccessToken, storeAccessToken, processMessages } from '@/lib/utils'; const SELECTED_MODEL_KEY = 'selectedModel'; -interface ContextFile { +export interface ContextFile { id: string; name: string; content: string; @@ -67,12 +70,12 @@ interface ChatContextType { // Chat management selectedChat: string | null; setSelectedChat: (chatId: string | null) => void; - chats: any[]; + chats: ChatHistory[]; handleNewChat: () => void; handleChatSelect: (chatId: string) => void; handleMessagesSelect: (messages: AIMessage[]) => void; - saveChat: (messages: AIMessage[], model: any, system: string) => string; - updateChat: (chatId: string, messages: AIMessage[], model?: any) => void; + saveChat: (messages: AIMessage[], model: Model, system: string) => string; + updateChat: (chatId: string, messages: AIMessage[], model?: Model) => void; deleteChat: (chatId: string) => void; renameChat: (chatId: string, newName: string) => void; moveToFolder: (chatId: string, folderId: string | null) => void; @@ -82,7 +85,7 @@ interface ChatContextType { handleBranch: (messageIndex: number) => void; // Folder management - folders: any[]; + folders: Folder[]; createFolder: (name: string) => string; updateFolder: (folderId: string, name: string) => void; deleteFolder: (folderId: string) => void; @@ -142,7 +145,6 @@ export const ChatProvider: React.FC<{ children: React.ReactNode }> = ({ children setInput, reload, stop, - error, } = useChat({ api: '/api/chat', body: { @@ -150,13 +152,13 @@ export const ChatProvider: React.FC<{ children: React.ReactNode }> = ({ children system: systemPrompt, temperature, topP, - context: contextFiles.map(f => ({ + context: contextFiles.map((f: ContextFile) => ({ content: f.content, name: f.name, type: f.type, })), }, - onFinish: (message) => { + onFinish: (message: AIMessage) => { setModelError(null); if (!selectedChat) { const chatId = saveChat([...messages, message], { @@ -166,7 +168,7 @@ export const ChatProvider: React.FC<{ children: React.ReactNode }> = ({ children setSelectedChat(chatId); } }, - onError: (error) => { + onError: (error: Error) => { setModelError(error.message); console.error('Chat error:', error); }, @@ -183,7 +185,7 @@ export const ChatProvider: React.FC<{ children: React.ReactNode }> = ({ children useEffect(() => { const init = async () => { try { - const statusResponse = await fetch('/api/auth/status'); + const statusResponse = await fetch('/api/auth/status/'); if (statusResponse.ok) { const { requiresAccessToken } = await statusResponse.json(); @@ -196,7 +198,7 @@ export const ChatProvider: React.FC<{ children: React.ReactNode }> = ({ children } const accessToken = getAccessToken(); - const response = await fetch('/api/auth/session', accessToken ? { + const response = await fetch('/api/auth/session/', accessToken ? { headers: { 'Authorization': `Bearer ${accessToken}` } @@ -219,14 +221,68 @@ export const ChatProvider: React.FC<{ children: React.ReactNode }> = ({ children init(); }, []); + useEffect(() => { + if (!sessionInitialized) {return;} + + // Refresh token every 20% of the cache TTL on visibility change + let lastRefreshTime = 0; + const MIN_REFRESH_INTERVAL = CACHE_TTL * 0.2 * 1000; + + const refreshToken = async () => { + try { + const response = await fetch('/api/auth/session/refresh/', { + method: 'POST', + credentials: 'include', + }); + + if (!response.ok) { + if (response.status === 401) { + const newSessionResponse = await fetch('/api/auth/session/'); + if (newSessionResponse.ok) { + return; + } + } + throw new Error('Failed to refresh session'); + } + } catch (error) { + console.error('Failed to refresh session:', error); + } + }; + + // Refresh token when 80% of the TTL has passed + const refreshInterval = CACHE_TTL * 0.8 * 1000; + const intervalId = setInterval(refreshToken, refreshInterval); + + // Add visibility change listener with debounce + const handleVisibilityChange = () => { + if (document.visibilityState === 'visible') { + const now = Date.now(); + if (now - lastRefreshTime >= MIN_REFRESH_INTERVAL) { + lastRefreshTime = now; + refreshToken(); + } + } + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + + // Initial refresh + refreshToken(); + + return () => { + clearInterval(intervalId); + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; + }, [sessionInitialized]); + // fetch models once on initialization useEffect(() => { - if (!sessionInitialized) return; + if (!sessionInitialized) {return;} const fetchModels = async () => { try { setIsLoadingModels(true); - const response = await fetch('/api/models'); + const response = await fetch('/api/models/'); if (!response.ok) { throw new Error('Failed to fetch models'); } @@ -261,7 +317,7 @@ export const ChatProvider: React.FC<{ children: React.ReactNode }> = ({ children }, [sessionInitialized]); useEffect(() => { - const model = availableModels.find(m => m.id === modelSelection); + const model = availableModels.find((m: Model) => m.id === modelSelection); if (model) { setTemperature(model.temperature || 0.7); setTopP(model.top_p || 0.95); @@ -281,7 +337,7 @@ export const ChatProvider: React.FC<{ children: React.ReactNode }> = ({ children await storeAccessToken(accessTokenInput.trim()); // Try to validate the token with the server - const response = await fetch('/api/auth/session', { + const response = await fetch('/api/auth/session/', { headers: { 'Authorization': `Bearer ${getAccessToken()}` } @@ -316,8 +372,8 @@ export const ChatProvider: React.FC<{ children: React.ReactNode }> = ({ children }; const handleChatSelect = (chatId: string) => { - if (isLoading) return; - const chat = chats.find(c => c.id === chatId); + if (isLoading) {return;} + const chat = chats.find((c: { id: string; }) => c.id === chatId); if (chat) { setSelectedChat(chatId); setModelSelection(chat.model.id); @@ -334,18 +390,18 @@ export const ChatProvider: React.FC<{ children: React.ReactNode }> = ({ children // If there's a selected chat, update the model information before submitting if (selectedChat) { - const currentChat = chats.find(chat => chat.id === selectedChat); + const currentChat = chats.find((chat: { id: string; }) => chat.id === selectedChat); if (currentChat && currentChat.model.id !== modelSelection) { // Update the existing chat with the new model updateChat(selectedChat, messages, { id: modelSelection, - name: availableModels.find(m => m.id === modelSelection)?.name || modelSelection, + name: availableModels.find((m: Model) => m.id === modelSelection)?.name || modelSelection, }); } } // Check if the model is available - const model = availableModels.find(m => m.id === modelSelection); + const model = availableModels.find((m: Model) => m.id === modelSelection); if (!model || !model.available) { setModelError('Model is not available. Please select a different model.'); return; @@ -363,10 +419,10 @@ export const ChatProvider: React.FC<{ children: React.ReactNode }> = ({ children // Handle branching from a specific message const handleBranch = (messageIndex: number) => { - if (!selectedChat || isLoading) return; + if (!selectedChat || isLoading) {return;} - const sourceChat = chats.find(chat => chat.id === selectedChat); - if (!sourceChat) return; + const sourceChat = chats.find((chat: { id: string; }) => chat.id === selectedChat); + if (!sourceChat) {return;} const branchedChatId = branchChat(selectedChat, messageIndex); diff --git a/app/layout.tsx b/app/layout.tsx index e8fa39e..ec80804 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,7 +1,8 @@ import './globals.css'; +import { GoogleTagManager } from '@next/third-parties/google'; import { Inter } from 'next/font/google'; import { ThemeProvider } from 'next-themes'; -import { GoogleTagManager } from '@next/third-parties/google'; + import { ChatProvider } from './context/ChatContext'; const inter = Inter({ subsets: ['latin'] }); diff --git a/app/models/[modelId]/chat/layout.tsx b/app/models/[modelId]/chat/layout.tsx index a661ae4..29e5194 100644 --- a/app/models/[modelId]/chat/layout.tsx +++ b/app/models/[modelId]/chat/layout.tsx @@ -1,7 +1,8 @@ import { Metadata, ResolvingMetadata } from 'next'; -import { models } from '@/app/config/models'; import { use } from 'react'; +import { models } from '@/app/config/models'; + // Dynamic metadata generation export async function generateMetadata( { params }: { params: Promise<{ modelId: string }> }, @@ -23,11 +24,9 @@ export async function generateMetadata( // Format model capabilities for description const capabilities = []; - if (model.tokenLimit) capabilities.push(`${(model.tokenLimit / 1000).toFixed(0)}K context window`); + if (model.tokenLimit) {capabilities.push(`${(model.tokenLimit / 1000).toFixed(0)}K context window`);} const modelFamily = model.id.split('-')[0]; - // todo: change this to a more accurate description - const detailedDescription = model.description ? `${model.description}${capabilities.length ? '. ' : ''}` : ''; diff --git a/app/models/[modelId]/chat/metadata.tsx b/app/models/[modelId]/chat/metadata.tsx index 8398f76..a03a3c1 100644 --- a/app/models/[modelId]/chat/metadata.tsx +++ b/app/models/[modelId]/chat/metadata.tsx @@ -1,4 +1,5 @@ import { Metadata } from 'next'; + import { models } from '@/app/config/models'; export async function generateMetadata(props: { diff --git a/app/models/[modelId]/chat/page.tsx b/app/models/[modelId]/chat/page.tsx index f98d8ca..4a2688c 100644 --- a/app/models/[modelId]/chat/page.tsx +++ b/app/models/[modelId]/chat/page.tsx @@ -1,20 +1,18 @@ 'use client'; -import { useChatContext } from "@/app/context/ChatContext"; -import { Message } from '@/components/message'; -import { AkashChatLogo } from '@/components/branding/akash-chat-logo'; import { AlertCircle, Loader2, X } from "lucide-react"; -import { motion } from "framer-motion"; -import { Model } from "@/app/config/models"; import { useRouter } from "next/navigation"; -import { use, useEffect, useState, useRef } from "react"; -import { Button } from "@/components/ui/button"; -import { AI_NOTICE } from '@/app/config/genimg'; +import { use, useEffect, useState } from "react"; + +import { Model } from "@/app/config/models"; +import { useChatContext } from "@/app/context/ChatContext"; +import { ChatMessages } from '@/components/chat/chat-messages'; import { ModelConfig } from '@/components/model-config'; -import { ChatInput } from '@/components/chat-input'; -import { useWindowSize } from "usehooks-ts"; +import { Button } from "@/components/ui/button"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export default function ModelDetailPage( {params}: any) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const promiseParams: any = use(params); const modelId = promiseParams?.modelId || ''; const router = useRouter(); @@ -53,45 +51,10 @@ export default function ModelDetailPage( {params}: any) { setTopP } = useChatContext(); - // Refs - const messagesEndRef = useRef(null); - const messagesContainerRef = useRef(null); - // Local state const [isModelLoading, setIsModelLoading] = useState(true); const [model, setModel] = useState(null); const [modelNotFound, setModelNotFound] = useState(false); - const [autoScroll, setAutoScroll] = useState(true); - const { width: windowWidth } = useWindowSize(); - const isMobile = windowWidth ? windowWidth < 768 : false; - - // Utility functions - const scrollToBottom = () => { - const container = messagesContainerRef.current; - if (container && autoScroll) { - container.scrollTop = container.scrollHeight; - } - }; - - // Handle scroll events to toggle auto-scroll - const handleScroll = () => { - const container = messagesContainerRef.current; - if (container) { - const { scrollTop, scrollHeight, clientHeight } = container; - const threshold = isMobile ? 50 : 20; - const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < threshold; - if (isAtBottom !== autoScroll) { - setAutoScroll(isAtBottom); - } - } - }; - - // Effect to scroll to bottom when messages change - useEffect(() => { - if (messages.length > 0) { - scrollToBottom(); - } - }, [messages]); // Effect to set model on mount useEffect(() => { @@ -233,103 +196,22 @@ export default function ModelDetailPage( {params}: any) { )} - {/* Chat Messages - same as main page */} -
-
- {messages.length === 0 ? ( -
-
- -
- {/* Add model-specific welcome message */} -
- -
-

- {AI_NOTICE} -

-
- ) : ( - <> - {messages.map((message, index) => ( - { - const precedingUserMessage = messages[index - 1]; - if (precedingUserMessage?.role === 'user') { - reload(); - } - } : undefined} - onBranch={selectedChat && !isLoading ? () => handleBranch(index) : undefined} - /> - ))} - {modelError && ( - - -

{modelError}

- {messages.length > 0 && messages[messages.length - 1].role === 'user' && ( - - )} -
- )} -
- - )} -
-
- - {/* Input Form - Only show when there are messages */} - {messages.length > 0 && ( -
- -

- {AI_NOTICE} -

-
- )} + { return ( { return ( diff --git a/components/branding/akash-sign-logo.tsx b/components/branding/akash-sign-logo.tsx index 056ffe2..51e3650 100644 --- a/components/branding/akash-sign-logo.tsx +++ b/components/branding/akash-sign-logo.tsx @@ -1,3 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export const AkashSignLogo = (props: any) => { return ( { return ( - + diff --git a/components/branding/llama-1.tsx b/components/branding/llama-1.tsx index 2c5e4e7..6018ed1 100644 --- a/components/branding/llama-1.tsx +++ b/components/branding/llama-1.tsx @@ -1,7 +1,8 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export const Llama1Logo = (props: any) => { return ( - + diff --git a/components/branding/llama-2.tsx b/components/branding/llama-2.tsx index be8303a..72bed6c 100644 --- a/components/branding/llama-2.tsx +++ b/components/branding/llama-2.tsx @@ -1,3 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export const Llama2Logo = (props: any) => { return ( diff --git a/components/branding/llama-3.tsx b/components/branding/llama-3.tsx index 1c16c33..a37f69d 100644 --- a/components/branding/llama-3.tsx +++ b/components/branding/llama-3.tsx @@ -1,7 +1,8 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export const Llama3Logo = (props: any) => { return ( - + diff --git a/components/branding/llama-4.tsx b/components/branding/llama-4.tsx index de5bd97..51e61bd 100644 --- a/components/branding/llama-4.tsx +++ b/components/branding/llama-4.tsx @@ -1,3 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export const Llama4Logo = (props: any) => { return ( diff --git a/components/branding/model-thumbnail.tsx b/components/branding/model-thumbnail.tsx index 448348d..b582410 100644 --- a/components/branding/model-thumbnail.tsx +++ b/components/branding/model-thumbnail.tsx @@ -1,7 +1,6 @@ -import { Model } from "@/app/config/models"; -import { Llama1Logo } from "./llama-1"; import { AkashGenLogo } from "./akash-gen"; import { DeepSeekLogo } from "./deepseek"; +import { Llama1Logo } from "./llama-1"; import { Llama2Logo } from "./llama-2"; import { Llama3Logo } from "./llama-3"; import { Llama4Logo } from "./llama-4"; diff --git a/components/chat-home.tsx b/components/chat-home.tsx deleted file mode 100644 index d7c32f1..0000000 --- a/components/chat-home.tsx +++ /dev/null @@ -1,249 +0,0 @@ -'use client'; - -import { useChatContext } from '@/app/context/ChatContext'; -import { Message } from '@/components/message'; -import { AkashChatLogo } from '@/components/branding/akash-chat-logo'; -import { AlertCircle } from 'lucide-react'; -import { motion } from 'framer-motion'; -import { ModelConfig } from '@/components/model-config'; -import { ChatInput } from '@/components/chat-input'; -import { MainLayout } from '@/components/layout/MainLayout'; -import { useEffect, useRef, useState } from 'react'; -import { AI_NOTICE } from '@/app/config/genimg'; -import { useWindowSize } from 'usehooks-ts'; - -export function ChatHome() { - const { - // Session and model state - sessionInitialized, - sessionError, - isAccessError, - accessTokenInput, - setAccessTokenInput, - handleAccessTokenSubmit, - modelSelection, - setModelSelection, - availableModels, - isLoadingModels, - modelError, - - // UI state - isSidebarOpen, - setSidebarOpen, - isConfigOpen, - setIsConfigOpen, - - // Chat state - messages, - input, - handleInputChange, - handleSubmit, - isLoading, - contextFiles, - setContextFiles, - reload, - stop, - - // Chat management - selectedChat, - handleChatSelect, - handleMessagesSelect, - handleNewChat, - chats, - deleteChat, - renameChat, - moveToFolder, - exportChats, - importChats, - handleBranch, - - // Folder management - folders, - createFolder, - updateFolder, - deleteFolder, - - // Config state - systemPrompt, - setSystemPrompt, - setTemperature, - setTopP - } = useChatContext(); - - // Refs - const messagesEndRef = useRef(null); - const messagesContainerRef = useRef(null); - - const [autoScroll, setAutoScroll] = useState(true); - const { width: windowWidth } = useWindowSize(); - const isMobile = windowWidth ? windowWidth < 768 : false; - - // Utility functions - const scrollToBottom = () => { - const container = messagesContainerRef.current; - if (container && autoScroll) { - container.scrollTop = container.scrollHeight; - } - }; - - // Handle scroll events to toggle auto-scroll - const handleScroll = () => { - const container = messagesContainerRef.current; - if (container) { - const { scrollTop, scrollHeight, clientHeight } = container; - const threshold = isMobile ? 50 : 20; - const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < threshold; - if (isAtBottom !== autoScroll) { - setAutoScroll(isAtBottom); - } - } - }; - - // Effect to scroll to bottom when messages change - useEffect(() => { - if (messages.length > 0) { - scrollToBottom(); - } - }, [messages]); - return ( - setIsConfigOpen(true)} - sessionInitialized={sessionInitialized} - sessionError={sessionError} - isAccessError={isAccessError} - accessTokenInput={accessTokenInput} - setAccessTokenInput={setAccessTokenInput} - handleAccessTokenSubmit={handleAccessTokenSubmit} - modelError={modelError} - > - {/* Chat Messages */} -
-
- {messages.length === 0 ? ( -
-
- -
-

What can I help you with?

-
- -
-

- {AI_NOTICE} -

-
- ) : ( - <> - {messages.map((message, index) => ( - { - const precedingUserMessage = messages[index - 1]; - if (precedingUserMessage?.role === 'user') { - reload(); - } - } : undefined} - onBranch={selectedChat && !isLoading ? () => handleBranch(index) : undefined} - /> - ))} - {modelError && ( - - -

{modelError}

- {messages.length > 0 && messages[messages.length - 1].role === 'user' && ( - - )} -
- )} -
- - )} -
-
- - {/* Input Form - Only show when there are messages */} - {messages.length > 0 && ( -
- -

- {AI_NOTICE} -

-
- )} - - - - ); -} \ No newline at end of file diff --git a/components/chat-header.tsx b/components/chat/chat-header.tsx similarity index 98% rename from components/chat-header.tsx rename to components/chat/chat-header.tsx index 9602cf1..7d40920 100644 --- a/components/chat-header.tsx +++ b/components/chat/chat-header.tsx @@ -1,12 +1,12 @@ 'use client'; -import { AkashChatLogo } from '@/components/branding/akash-chat-logo'; -import { PanelRightClose, PanelRightOpen, LoaderCircle, Package } from 'lucide-react'; -import { Model } from '@/app/config/models'; -import { cn } from '@/lib/utils'; -import { ThemeToggle } from '@/components/theme-toggle'; +import { PanelRightClose, PanelRightOpen, LoaderCircle } from 'lucide-react'; import Link from 'next/link'; import { useRouter, usePathname } from 'next/navigation'; + +import { Model } from '@/app/config/models'; +import { AkashChatLogo } from '@/components/branding/akash-chat-logo'; +import { ThemeToggle } from '@/components/theme-toggle'; import { DropdownMenu, DropdownMenuContent, @@ -14,6 +14,7 @@ import { DropdownMenuRadioItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { cn } from '@/lib/utils'; interface ChatHeaderProps { modelSelection: string; @@ -38,7 +39,6 @@ export function ChatHeader({ // Check if we're on a model detail page const isModelDetailPage = pathname && pathname.startsWith('/models/') && !pathname.endsWith('/chat/'); const isChatPage = pathname && pathname.startsWith('/models/') && pathname.endsWith('/chat/'); - // Handle model selection with optional redirection const handleModelChange = (newModelId: string) => { diff --git a/components/chat/chat-home.tsx b/components/chat/chat-home.tsx new file mode 100644 index 0000000..6898234 --- /dev/null +++ b/components/chat/chat-home.tsx @@ -0,0 +1,126 @@ +'use client'; + +import { useChatContext } from '@/app/context/ChatContext'; +import { ChatMessages } from '@/components/chat/chat-messages'; +import { MainLayout } from '@/components/layout/MainLayout'; +import { ModelConfig } from '@/components/model-config'; + +export function ChatHome() { + const { + // Session and model state + sessionInitialized, + sessionError, + isAccessError, + accessTokenInput, + setAccessTokenInput, + handleAccessTokenSubmit, + modelSelection, + setModelSelection, + availableModels, + isLoadingModels, + modelError, + + // UI state + isSidebarOpen, + setSidebarOpen, + isConfigOpen, + setIsConfigOpen, + + // Chat state + messages, + input, + handleInputChange, + handleSubmit, + isLoading, + contextFiles, + setContextFiles, + reload, + stop, + + // Chat management + selectedChat, + handleChatSelect, + handleMessagesSelect, + handleNewChat, + chats, + deleteChat, + renameChat, + moveToFolder, + exportChats, + importChats, + handleBranch, + + // Folder management + folders, + createFolder, + updateFolder, + deleteFolder, + + // Config state + systemPrompt, + setSystemPrompt, + setTemperature, + setTopP + } = useChatContext(); + + return ( + setIsConfigOpen(true)} + sessionInitialized={sessionInitialized} + sessionError={sessionError} + isAccessError={isAccessError} + accessTokenInput={accessTokenInput} + setAccessTokenInput={setAccessTokenInput} + handleAccessTokenSubmit={handleAccessTokenSubmit} + modelError={modelError} + > + + + + + ); +} \ No newline at end of file diff --git a/components/chat-input.tsx b/components/chat/chat-input.tsx similarity index 82% rename from components/chat-input.tsx rename to components/chat/chat-input.tsx index 6c06df2..f75b37b 100644 --- a/components/chat-input.tsx +++ b/components/chat/chat-input.tsx @@ -1,26 +1,14 @@ -import { cn } from "@/lib/utils"; -import { Textarea } from "@/components/ui/textarea"; import { Mic, MicOff, LoaderCircle, Paperclip, X } from 'lucide-react'; -import { useWebSocketTranscription } from '@/hooks/use-websocket-transcription'; -import { Button } from "./ui/button"; import { useState, useRef, useEffect } from "react"; -import { FileUpload } from './file-upload'; -import * as pdfjsLib from 'pdfjs-dist'; -import { GlobalWorkerOptions } from 'pdfjs-dist'; - -// Initialize PDF.js worker -if (typeof window !== 'undefined') { - GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/4.10.38/pdf.worker.min.mjs`; -} +import { ContextFile } from "@/app/context/ChatContext"; +import { Textarea } from "@/components/ui/textarea"; +import { useWebSocketTranscription } from '@/hooks/use-websocket-transcription'; +import { cn } from "@/lib/utils"; +import { handleFileSelect } from '@/utils/file-handler'; -interface ContextFile { - id: string; - name: string; - content: string; - type: string; - preview?: string; -} +import { FileUpload } from '../file-upload'; +import { Button } from "../ui/button"; interface ChatInputProps { input: string; @@ -35,23 +23,6 @@ interface ChatInputProps { isInitialized?: boolean; } -const extractPdfText = async (file: File): Promise => { - const arrayBuffer = await file.arrayBuffer(); - const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; - let fullText = ''; - - for (let i = 1; i <= pdf.numPages; i++) { - const page = await pdf.getPage(i); - const textContent = await page.getTextContent(); - const pageText = textContent.items - .map((item: any) => item.str) - .join(' '); - fullText += pageText + '\n\n'; - } - - return fullText; -}; - export function ChatInput({ input, isLoading, @@ -80,8 +51,9 @@ export function ChatInput({ } as React.ChangeEvent; onChange(event); - }, - onError: (error:any) => { + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onError: (error: any) => { console.error('Transcription error:', error); }, isInitialized: isInitialized @@ -101,49 +73,9 @@ export function ChatInput({ } }; - const handleFileSelect = async (files: File[]) => { + const handleFilesSelected = async (files: File[]) => { if (onFilesChange) { - const newFiles = await Promise.all( - files.map(async (file) => { - let content: string; - let type = file.type || 'text/plain'; - const extension = file.name.split('.').pop()?.toLowerCase(); - - // Handle different file types - if (extension === 'pdf' || type === 'application/pdf') { - type = 'application/pdf'; - content = await extractPdfText(file); - } else { - content = await file.text(); - } - - const preview = content.slice(0, 500) + (content.length > 500 ? '...' : ''); - - // Set the correct MIME type based on extension - if (extension) { - switch (extension) { - case 'md': - type = 'text/markdown'; - break; - case 'json': - type = 'application/json'; - break; - case 'txt': - type = 'text/plain'; - break; - } - } - - return { - id: Math.random().toString(36).slice(2), - name: file.name, - content, - type, - preview, - }; - }) - ); - + const newFiles = await handleFileSelect(files); const updatedFiles = [...contextFiles, ...newFiles]; onFilesChange(updatedFiles); setShowFileUpload(false); @@ -194,7 +126,7 @@ export function ChatInput({ {showFileUpload && (
)} -