From 0ed770af5708dd810dd17bc5cdaa2545d1a77971 Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Tue, 22 Oct 2024 15:17:36 -0500 Subject: [PATCH 1/7] chore: adding turbo dev --- .gitignore | 20 ++++++++--- backend/package.json | 1 + backend/src/schema.gql | 7 ++-- llm-server/package.json | 1 + package.json | 23 ++++++++++++ pnpm-lock.yaml | 78 +++++++++++++++++++++++++++++++++++++++++ pnpm-workspace.yaml | 4 +++ turbo.json | 32 +++++++++++++++++ 8 files changed, 157 insertions(+), 9 deletions(-) create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 turbo.json diff --git a/.gitignore b/.gitignore index 93f2670..0440d74 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,15 @@ -*/**.turbo/ -*/**/node_modules -*/**/dist -# temp model -*/**/models \ No newline at end of file +# Dependencies +node_modules/ +*/**/node_modules/ + +# Turbo +.turbo/ +*/**/.turbo/ + +# Build outputs +dist/ +*/**/dist/ + +# Models +models/ +*/**/models/ \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index 89b614c..3952c29 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,6 +11,7 @@ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start --watch", "start:dev": "nest start --watch", + "dev:backend": "pnpm start:dev", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", diff --git a/backend/src/schema.gql b/backend/src/schema.gql index ebc8efd..d0dec96 100644 --- a/backend/src/schema.gql +++ b/backend/src/schema.gql @@ -53,7 +53,6 @@ type Menu { } type Mutation { - addPackageToProject(packageContent: String!, projectId: String!): ProjectPackages! deleteProject(projectId: String!): Boolean! login(input: LoginUserInput!): LoginResponse! registerUser(input: RegisterUserInput!): User! @@ -87,7 +86,7 @@ type Projects { type Query { checkToken(input: CheckTokenInput!): Boolean! getProjectDetails(projectId: String!): Projects! - getUserProjects(userId: String!): [Projects!]! + getUserProjects: [Projects!]! logout: Boolean! } @@ -99,12 +98,12 @@ input RegisterUserInput { type Subscription { chatStream(input: ChatInputType!): ChatCompletionChunkType +} input UpsertProjectInput { - path: String! project_id: ID project_name: String! - user_id: ID! + project_packages: [String!] } type User { diff --git a/llm-server/package.json b/llm-server/package.json index bb99653..1447d54 100644 --- a/llm-server/package.json +++ b/llm-server/package.json @@ -6,6 +6,7 @@ "scripts": { "start": "node --experimental-specifier-resolution=node --loader ts-node/esm src/main.ts", "start:dev": "nodemon --watch 'src/**/*.ts' --exec 'node --experimental-specifier-resolution=node --loader ts-node/esm' src/main.ts", + "dev:backend": "pnpm start:dev", "build": "tsc", "serve": "node --experimental-specifier-resolution=node dist/main.js" }, diff --git a/package.json b/package.json new file mode 100644 index 0000000..1f018fc --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "GeneratorAI", + "version": "1.0.0", + "description": "", + "main": "index.js", + "private": true, + "scripts": { + "build": "turbo build", + "dev": "turbo dev", + "lint": "turbo lint", + "dev:backend": "turbo dev:backend" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "turbo": "^2.2.3" + }, + "packageManager": "pnpm@9.1.2", + "engines": { + "node": ">=18" + } +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..5c5f94b --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,78 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + turbo: + specifier: ^2.2.3 + version: 2.2.3 + +packages: + + turbo-darwin-64@2.2.3: + resolution: {integrity: sha512-Rcm10CuMKQGcdIBS3R/9PMeuYnv6beYIHqfZFeKWVYEWH69sauj4INs83zKMTUiZJ3/hWGZ4jet9AOwhsssLyg==} + cpu: [x64] + os: [darwin] + + turbo-darwin-arm64@2.2.3: + resolution: {integrity: sha512-+EIMHkuLFqUdJYsA3roj66t9+9IciCajgj+DVek+QezEdOJKcRxlvDOS2BUaeN8kEzVSsNiAGnoysFWYw4K0HA==} + cpu: [arm64] + os: [darwin] + + turbo-linux-64@2.2.3: + resolution: {integrity: sha512-UBhJCYnqtaeOBQLmLo8BAisWbc9v9daL9G8upLR+XGj6vuN/Nz6qUAhverN4Pyej1g4Nt1BhROnj6GLOPYyqxQ==} + cpu: [x64] + os: [linux] + + turbo-linux-arm64@2.2.3: + resolution: {integrity: sha512-hJYT9dN06XCQ3jBka/EWvvAETnHRs3xuO/rb5bESmDfG+d9yQjeTMlhRXKrr4eyIMt6cLDt1LBfyi+6CQ+VAwQ==} + cpu: [arm64] + os: [linux] + + turbo-windows-64@2.2.3: + resolution: {integrity: sha512-NPrjacrZypMBF31b4HE4ROg4P3nhMBPHKS5WTpMwf7wydZ8uvdEHpESVNMOtqhlp857zbnKYgP+yJF30H3N2dQ==} + cpu: [x64] + os: [win32] + + turbo-windows-arm64@2.2.3: + resolution: {integrity: sha512-fnNrYBCqn6zgKPKLHu4sOkihBI/+0oYFr075duRxqUZ+1aLWTAGfHZLgjVeLh3zR37CVzuerGIPWAEkNhkWEIw==} + cpu: [arm64] + os: [win32] + + turbo@2.2.3: + resolution: {integrity: sha512-5lDvSqIxCYJ/BAd6rQGK/AzFRhBkbu4JHVMLmGh/hCb7U3CqSnr5Tjwfy9vc+/5wG2DJ6wttgAaA7MoCgvBKZQ==} + hasBin: true + +snapshots: + + turbo-darwin-64@2.2.3: + optional: true + + turbo-darwin-arm64@2.2.3: + optional: true + + turbo-linux-64@2.2.3: + optional: true + + turbo-linux-arm64@2.2.3: + optional: true + + turbo-windows-64@2.2.3: + optional: true + + turbo-windows-arm64@2.2.3: + optional: true + + turbo@2.2.3: + optionalDependencies: + turbo-darwin-64: 2.2.3 + turbo-darwin-arm64: 2.2.3 + turbo-linux-64: 2.2.3 + turbo-linux-arm64: 2.2.3 + turbo-windows-64: 2.2.3 + turbo-windows-arm64: 2.2.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..640fe64 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +packages: + - "backend" + - "frontend" + - "llm-server" diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000..ae7fccc --- /dev/null +++ b/turbo.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://turbo.build/schema.json", + "ui": "tui", + "tasks": { + "build": { + "dependsOn": [ + "^build" + ], + "inputs": [ + "$TURBO_DEFAULT$", + ".env*" + ], + "outputs": [ + ".next/**", + "!.next/cache/**" + ] + }, + "lint": { + "dependsOn": [ + "^lint" + ] + }, + "dev": { + "cache": false, + "persistent": true + }, + "dev:backend": { + "cache": false, + "persistent": true + } + } +} \ No newline at end of file From bc5466919b74ad57924aad2493f3637d3b47f602 Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Tue, 22 Oct 2024 15:18:57 -0500 Subject: [PATCH 2/7] chore: adding dev --- backend/package.json | 1 + llm-server/package.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/package.json b/backend/package.json index 3952c29..7823cfd 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,6 +11,7 @@ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start --watch", "start:dev": "nest start --watch", + "dev": "pnpm start:dev", "dev:backend": "pnpm start:dev", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", diff --git a/llm-server/package.json b/llm-server/package.json index 1447d54..84ae999 100644 --- a/llm-server/package.json +++ b/llm-server/package.json @@ -5,8 +5,8 @@ "type": "module", "scripts": { "start": "node --experimental-specifier-resolution=node --loader ts-node/esm src/main.ts", - "start:dev": "nodemon --watch 'src/**/*.ts' --exec 'node --experimental-specifier-resolution=node --loader ts-node/esm' src/main.ts", - "dev:backend": "pnpm start:dev", + "dev": "nodemon --watch 'src/**/*.ts' --exec 'node --experimental-specifier-resolution=node --loader ts-node/esm' src/main.ts", + "dev:backend": "pnpm dev", "build": "tsc", "serve": "node --experimental-specifier-resolution=node dist/main.js" }, From a2111cab00d66f72c2acd8f29e7d7e7a44720d33 Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Tue, 22 Oct 2024 15:22:11 -0500 Subject: [PATCH 3/7] chore: adding test and build --- backend/package.json | 2 +- package.json | 3 ++- turbo.json | 5 +++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/package.json b/backend/package.json index 7823cfd..dda4b27 100644 --- a/backend/package.json +++ b/backend/package.json @@ -16,7 +16,7 @@ "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest", + "test": "jest --passWithNoTests", "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", diff --git a/package.json b/package.json index 1f018fc..591bdcc 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "build": "turbo build", "dev": "turbo dev", "lint": "turbo lint", - "dev:backend": "turbo dev:backend" + "dev:backend": "turbo dev:backend", + "test": "turbo test" }, "keywords": [], "author": "", diff --git a/turbo.json b/turbo.json index ae7fccc..683ed20 100644 --- a/turbo.json +++ b/turbo.json @@ -27,6 +27,11 @@ "dev:backend": { "cache": false, "persistent": true + }, + "test": { + "dependsOn": [ + "^test" + ] } } } \ No newline at end of file From c83d5c7e7029ac81d397ba9b7fe1afb202e6adc2 Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Tue, 22 Oct 2024 15:32:19 -0500 Subject: [PATCH 4/7] chore: adding diff port for backend --- backend/.env | 3 +++ backend/.env.development | 2 +- backend/.gitignore | 6 ------ 3 files changed, 4 insertions(+), 7 deletions(-) create mode 100644 backend/.env diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..8d548c2 --- /dev/null +++ b/backend/.env @@ -0,0 +1,3 @@ +PORT=8080 +JWT_SECRET="JACKSONCHENNAHEULALLEN" +SALT_ROUNDS=123 \ No newline at end of file diff --git a/backend/.env.development b/backend/.env.development index 5e9c8d6..8d548c2 100644 --- a/backend/.env.development +++ b/backend/.env.development @@ -1,3 +1,3 @@ -PORT=3000 +PORT=8080 JWT_SECRET="JACKSONCHENNAHEULALLEN" SALT_ROUNDS=123 \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore index 4b56acf..5d12180 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -35,12 +35,6 @@ lerna-debug.log* !.vscode/launch.json !.vscode/extensions.json -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local # temp directory .temp From 3830cd1b05299cd7fcee26b509672e1191037e3e Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Tue, 22 Oct 2024 23:20:28 -0500 Subject: [PATCH 5/7] chore: adding format and lint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit chore: adding workflow chore: Update pnpm version to 8 and use pnpm for installing dependencies chore: Update pnpm installation command to not use frozen lockfile chore: Add format and lint steps to workflow chore: Remove unused code and update autofix action style: 🎨 auto format and fix - Applied Prettier formatting - Applied ESLint fixes ⚠️ Some ESLint issues remain ⚠️ TypeScript errors detected chore: Update workflow to remove package.json sorting step chore: Update workflow to remove package.json sorting step --- .eslintignore | 6 + .eslintrc.js | 23 + .github/workflows/autofix.yml | 73 + .prettierignore | 7 + .prettierrc.js | 9 + backend/.eslintrc.js | 8 + backend/package.json | 11 +- backend/src/auth/auth.service.ts | 2 +- backend/src/chat/protocol.ts | 19 +- backend/src/decorator/get-auth-token.ts | 13 +- backend/src/guard/project.guard.ts | 101 +- backend/src/project/project.model.ts | 8 +- backend/src/project/project.resolver.ts | 33 +- backend/src/project/project.service.ts | 105 +- backend/src/user/user.resolver.ts | 2 +- frontend/package.json | 7 +- frontend/src/app/[id]/page.tsx | 77 +- frontend/src/app/api/chat/route.ts | 19 +- frontend/src/app/api/model/route.ts | 72 +- frontend/src/app/api/tags/route.ts | 9 +- frontend/src/app/hooks/useChatStore.ts | 27 +- frontend/src/app/hooks/useLocalStorageData.ts | 2 +- .../src/app/hooks/useSpeechRecognition.ts | 18 +- frontend/src/app/layout.tsx | 20 +- frontend/src/app/page.tsx | 106 +- .../src/components/chat/chat-bottombar.tsx | 77 +- frontend/src/components/chat/chat-layout.tsx | 28 +- frontend/src/components/chat/chat-list.tsx | 75 +- frontend/src/components/chat/chat-topbar.tsx | 49 +- frontend/src/components/chat/chat.tsx | 16 +- .../src/components/code-display-block.tsx | 24 +- .../src/components/edit-username-form.tsx | 47 +- frontend/src/components/emoji-picker.tsx | 51 +- frontend/src/components/image-embedder.tsx | 37 +- frontend/src/components/mode-toggle.tsx | 32 +- frontend/src/components/pull-model-form.tsx | 99 +- frontend/src/components/pull-model.tsx | 17 +- frontend/src/components/sidebar-skeleton.tsx | 3 +- frontend/src/components/sidebar.tsx | 58 +- frontend/src/components/ui/avatar.tsx | 28 +- frontend/src/components/ui/button.tsx | 42 +- frontend/src/components/ui/card.tsx | 49 +- frontend/src/components/ui/dialog.tsx | 56 +- frontend/src/components/ui/dropdown-menu.tsx | 94 +- frontend/src/components/ui/form.tsx | 105 +- frontend/src/components/ui/input.tsx | 14 +- frontend/src/components/ui/label.tsx | 20 +- frontend/src/components/ui/popover.tsx | 24 +- frontend/src/components/ui/resizable.tsx | 22 +- frontend/src/components/ui/select.tsx | 72 +- frontend/src/components/ui/sheet.tsx | 72 +- frontend/src/components/ui/skeleton.tsx | 8 +- frontend/src/components/ui/sonner.tsx | 26 +- frontend/src/components/ui/textarea.tsx | 14 +- frontend/src/components/ui/tooltip.tsx | 22 +- frontend/src/components/user-settings.tsx | 47 +- frontend/src/components/username-form.tsx | 59 +- frontend/src/lib/model-helper.ts | 16 +- frontend/src/lib/utils.ts | 6 +- frontend/src/providers/theme-provider.tsx | 10 +- frontend/src/utils/initial-questions.ts | 20 +- llm-server/.eslintrc.cjs | 35 + llm-server/.prettierrc.cjs | 14 + llm-server/package.json | 10 +- llm-server/src/llm-provider.ts | 20 +- llm-server/src/main.ts | 38 +- llm-server/src/model/llama-model-provider.ts | 38 +- llm-server/src/model/model-provider.ts | 4 +- llm-server/src/model/openai-model-provider.ts | 36 +- llm-server/src/protocol.ts | 21 +- package.json | 8 +- pnpm-lock.yaml | 15234 +++++++++++++++- turbo.json | 14 +- 73 files changed, 16578 insertions(+), 1110 deletions(-) create mode 100644 .eslintignore create mode 100644 .eslintrc.js create mode 100644 .github/workflows/autofix.yml create mode 100644 .prettierignore create mode 100644 .prettierrc.js create mode 100644 llm-server/.eslintrc.cjs create mode 100644 llm-server/.prettierrc.cjs diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..e8fa391 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,6 @@ +node_modules +dist +coverage +.github +.husky +*.config.js \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..12fca5d --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,23 @@ +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + plugins: ['@typescript-eslint'], + ignorePatterns: ['node_modules', 'dist', '.turbo', '.next', 'build'], + rules: { + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + 'no-console': 'warn', + 'prefer-const': 'error', + 'no-var': 'error', + }, +}; diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml new file mode 100644 index 0000000..323c75c --- /dev/null +++ b/.github/workflows/autofix.yml @@ -0,0 +1,73 @@ +name: autofix.ci + +on: + pull_request: + push: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + autofix: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + + - uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --no-frozen-lockfile + + - name: Cache pnpm modules + uses: actions/cache@v3 + with: + path: ~/.pnpm-store + key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm- + + - name: Format code with Prettier + id: format + run: | + pnpm exec prettier --write "**/*.{js,jsx,ts,tsx,json,md}" + if [ -n "$(git status --porcelain)" ]; then + echo "FORMAT_HAS_CHANGES=true" >> $GITHUB_ENV + fi + git add . + + - name: Run ESLint fix + id: lint + continue-on-error: true + run: | + pnpm exec eslint . --ext .js,.jsx,.ts,.tsx --fix + if [ -n "$(git status --porcelain)" ]; then + echo "LINT_HAS_CHANGES=true" >> $GITHUB_ENV + fi + # Run ESLint again to check remaining issues + pnpm exec eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0 || echo "LINT_HAS_ERRORS=true" >> $GITHUB_ENV + git add . + + - name: Check TypeScript + continue-on-error: true + run: | + pnpm exec tsc --noEmit + if [ $? -ne 0 ]; then + echo "TS_HAS_ERRORS=true" >> $GITHUB_ENV + fi + - uses: autofix-ci/action@dd55f44df8f7cdb7a6bf74c78677eb8acd40cd0a \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..544dc9b --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +node_modules +dist +coverage +.github +package-lock.json +yarn.lock +pnpm-lock.yaml \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..ea43de9 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,9 @@ +module.exports = { + singleQuote: true, + trailingComma: "es5", + printWidth: 80, + tabWidth: 2, + semi: true, + bracketSpacing: true, + endOfLine: "lf", +}; diff --git a/backend/.eslintrc.js b/backend/.eslintrc.js index 259de13..7661579 100644 --- a/backend/.eslintrc.js +++ b/backend/.eslintrc.js @@ -9,6 +9,7 @@ module.exports = { extends: [ 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', + '../.eslintrc.js', ], root: true, env: { @@ -21,5 +22,12 @@ module.exports = { '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-explicit-any': 'off', + 'prefer-const': [ + 'error', + { + destructuring: 'all', + ignoreReadBeforeAssign: true, + }, + ], }, }; diff --git a/backend/package.json b/backend/package.json index dda4b27..6276757 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,5 +1,5 @@ { - "name": "generator-ai", + "name": "codefox-backend", "version": "0.0.1", "description": "", "author": "", @@ -9,13 +9,13 @@ "scripts": { "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "lint": "ts-prune \"{src,apps,libs,test}/**/*.ts\" && eslint \"{src,apps,libs,test}/**/*.ts\" --fix ", "start": "nest start --watch", "start:dev": "nest start --watch", "dev": "pnpm start:dev", "dev:backend": "pnpm start:dev", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest --passWithNoTests", "test:watch": "jest --watch", "test:cov": "jest --coverage", @@ -44,6 +44,7 @@ "typeorm": "^0.3.20" }, "devDependencies": { + "@eslint/eslintrc": "^3.1.0", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", @@ -53,7 +54,7 @@ "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", - "eslint": "^9.0.0", + "eslint": "^8.57.1", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", "jest": "^29.5.0", @@ -63,8 +64,10 @@ "ts-jest": "^29.1.0", "ts-loader": "^9.4.3", "ts-node": "^10.9.1", + "ts-prune": "^0.10.3", "tsconfig-paths": "^4.2.0", - "typescript": "^5.1.3" + "typescript": "^5.1.3", + "typescript-eslint": "^8.11.0" }, "jest": { "moduleFileExtensions": [ diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index dd662ae..8c1e35a 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -81,7 +81,7 @@ export class AuthService { return false; } } - async logout(token: string): Promise { + async logout(token: string): Promise { Logger.log('logout token', token); try { await this.jwtService.verifyAsync(token); diff --git a/backend/src/chat/protocol.ts b/backend/src/chat/protocol.ts index be47564..3c9601a 100644 --- a/backend/src/chat/protocol.ts +++ b/backend/src/chat/protocol.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-namespace */ export interface ChatCompletionChunk { /** * A unique identifier for the chat completion. Each chunk has the same ID. @@ -25,13 +26,13 @@ export interface ChatCompletionChunk { /** * The object type, which is always `chat.completion.chunk`. */ - object: "chat.completion.chunk"; + object: 'chat.completion.chunk'; /** * The service tier used for processing the request. This field is only included if * the `service_tier` parameter is specified in the request. */ - service_tier?: "scale" | "default" | null; + service_tier?: 'scale' | 'default' | null; /** * This fingerprint represents the backend configuration that the model runs with. @@ -57,11 +58,11 @@ export namespace ChatCompletionChunk { * function. */ finish_reason: - | "stop" - | "length" - | "tool_calls" - | "content_filter" - | "function_call" + | 'stop' + | 'length' + | 'tool_calls' + | 'content_filter' + | 'function_call' | null; /** @@ -94,7 +95,7 @@ export namespace ChatCompletionChunk { /** * The role of the author of this message. */ - role?: "system" | "user" | "assistant" | "tool"; + role?: 'system' | 'user' | 'assistant' | 'tool'; tool_calls?: Array; } @@ -132,7 +133,7 @@ export namespace ChatCompletionChunk { /** * The type of the tool. Currently, only `function` is supported. */ - type?: "function"; + type?: 'function'; } export namespace ToolCall { diff --git a/backend/src/decorator/get-auth-token.ts b/backend/src/decorator/get-auth-token.ts index b90fd46..45a515e 100644 --- a/backend/src/decorator/get-auth-token.ts +++ b/backend/src/decorator/get-auth-token.ts @@ -1,4 +1,8 @@ -import { createParamDecorator, ExecutionContext, UnauthorizedException } from '@nestjs/common'; +import { + createParamDecorator, + ExecutionContext, + UnauthorizedException, +} from '@nestjs/common'; import { GqlExecutionContext } from '@nestjs/graphql'; import { JwtService } from '@nestjs/jwt'; @@ -15,7 +19,6 @@ export const GetAuthToken = createParamDecorator( }, ); - export const GetUserIdFromToken = createParamDecorator( (data: unknown, context: ExecutionContext) => { const ctx = GqlExecutionContext.create(context); @@ -23,7 +26,9 @@ export const GetUserIdFromToken = createParamDecorator( const authHeader = request.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { - throw new UnauthorizedException('Authorization token is missing or invalid'); + throw new UnauthorizedException( + 'Authorization token is missing or invalid', + ); } const token = authHeader.split(' ')[1]; @@ -36,4 +41,4 @@ export const GetUserIdFromToken = createParamDecorator( return decodedToken.userId; }, -); \ No newline at end of file +); diff --git a/backend/src/guard/project.guard.ts b/backend/src/guard/project.guard.ts index 03d5134..5458a35 100644 --- a/backend/src/guard/project.guard.ts +++ b/backend/src/guard/project.guard.ts @@ -1,56 +1,55 @@ import { - Injectable, - CanActivate, - ExecutionContext, - UnauthorizedException, - } from '@nestjs/common'; - import { GqlExecutionContext } from '@nestjs/graphql'; + Injectable, + CanActivate, + ExecutionContext, + UnauthorizedException, +} from '@nestjs/common'; +import { GqlExecutionContext } from '@nestjs/graphql'; import { JwtService } from '@nestjs/jwt'; import { ProjectsService } from '../project/project.service'; - - @Injectable() - export class ProjectGuard implements CanActivate { - constructor( - private readonly projectsService: ProjectsService, - private readonly jwtService: JwtService, - ) {} - - async canActivate(context: ExecutionContext): Promise { - const gqlContext = GqlExecutionContext.create(context); - const request = gqlContext.getContext().req; - - // Extract the authorization header - const authHeader = request.headers.authorization; - if (!authHeader || !authHeader.startsWith('Bearer ')) { - throw new UnauthorizedException('Authorization token is missing'); - } - - // Decode the token to get user information - const token = authHeader.split(' ')[1]; - let user: any; - try { - user = this.jwtService.verify(token); - } catch (error) { - throw new UnauthorizedException('Invalid token'); - } - - // Extract projectId from the request arguments - const args = gqlContext.getArgs(); - const { projectId } = args; - - // Fetch the project and check if the userId matches the project's userId - const project = await this.projectsService.getProjectById(projectId); - if (!project) { - throw new UnauthorizedException('Project not found'); - } - - //To do: In the feature when we need allow teams add check here - - if (project.user_id !== user.userId) { - throw new UnauthorizedException('User is not the owner of the project'); - } - - return true; + +@Injectable() +export class ProjectGuard implements CanActivate { + constructor( + private readonly projectsService: ProjectsService, + private readonly jwtService: JwtService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const gqlContext = GqlExecutionContext.create(context); + const request = gqlContext.getContext().req; + + // Extract the authorization header + const authHeader = request.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + throw new UnauthorizedException('Authorization token is missing'); } + + // Decode the token to get user information + const token = authHeader.split(' ')[1]; + let user: any; + try { + user = this.jwtService.verify(token); + } catch (error) { + throw new UnauthorizedException('Invalid token'); + } + + // Extract projectId from the request arguments + const args = gqlContext.getArgs(); + const { projectId } = args; + + // Fetch the project and check if the userId matches the project's userId + const project = await this.projectsService.getProjectById(projectId); + if (!project) { + throw new UnauthorizedException('Project not found'); + } + + //To do: In the feature when we need allow teams add check here + + if (project.user_id !== user.userId) { + throw new UnauthorizedException('User is not the owner of the project'); + } + + return true; } - \ No newline at end of file +} diff --git a/backend/src/project/project.model.ts b/backend/src/project/project.model.ts index ffc7688..befe5f9 100644 --- a/backend/src/project/project.model.ts +++ b/backend/src/project/project.model.ts @@ -33,8 +33,12 @@ export class Projects extends SystemBaseModel { @ManyToOne(() => User) @JoinColumn({ name: 'user_id' }) user: User; - + @Field(() => [ProjectPackages], { nullable: true }) - @OneToMany(() => ProjectPackages, (projectPackage) => projectPackage.project, { cascade: true }) + @OneToMany( + () => ProjectPackages, + (projectPackage) => projectPackage.project, + { cascade: true }, + ) projectPackages: ProjectPackages[]; } diff --git a/backend/src/project/project.resolver.ts b/backend/src/project/project.resolver.ts index ef6f81d..06b214b 100644 --- a/backend/src/project/project.resolver.ts +++ b/backend/src/project/project.resolver.ts @@ -1,11 +1,11 @@ // GraphQL Resolvers for Project APIs import { - Args, - Field, - Mutation, - ObjectType, - Query, - Resolver, + Args, + Field, + Mutation, + ObjectType, + Query, + Resolver, } from '@nestjs/graphql'; import { ProjectsService } from './project.service'; import { Projects } from './project.model'; @@ -16,25 +16,28 @@ import { GetUserIdFromToken } from '../decorator/get-auth-token'; @Resolver(() => Projects) export class ProjectsResolver { - constructor( - private readonly projectsService: ProjectsService, - ) {} + constructor(private readonly projectsService: ProjectsService) {} @Query(() => [Projects]) - async getUserProjects(@GetUserIdFromToken() userId: string): Promise { + async getUserProjects( + @GetUserIdFromToken() userId: string, + ): Promise { return this.projectsService.getProjectsByUser(userId); } // @GetAuthToken() token: string @Query(() => Projects) @UseGuards(ProjectGuard) - async getProjectDetails(@Args('projectId') projectId: string): Promise { + async getProjectDetails( + @Args('projectId') projectId: string, + ): Promise { return this.projectsService.getProjectById(projectId); } @Mutation(() => Projects) - async upsertProject(@GetUserIdFromToken() userId: string, - @Args('upsertProjectInput') upsertProjectInput: UpsertProjectInput + async upsertProject( + @GetUserIdFromToken() userId: string, + @Args('upsertProjectInput') upsertProjectInput: UpsertProjectInput, ): Promise { return this.projectsService.upsertProject(upsertProjectInput, userId); } @@ -49,7 +52,7 @@ export class ProjectsResolver { @UseGuards(ProjectGuard) async updateProjectPath( @Args('projectId') projectId: string, - @Args('newPath') newPath: string + @Args('newPath') newPath: string, ): Promise { return this.projectsService.updateProjectPath(projectId, newPath); } @@ -58,7 +61,7 @@ export class ProjectsResolver { @UseGuards(ProjectGuard) async removePackageFromProject( @Args('projectId') projectId: string, - @Args('packageId') packageId: string + @Args('packageId') packageId: string, ): Promise { return this.projectsService.removePackageFromProject(projectId, packageId); } diff --git a/backend/src/project/project.service.ts b/backend/src/project/project.service.ts index a57deaa..4fcae09 100644 --- a/backend/src/project/project.service.ts +++ b/backend/src/project/project.service.ts @@ -1,5 +1,9 @@ // Project Service for managing Projects -import { Injectable, NotFoundException, InternalServerErrorException } from '@nestjs/common'; +import { + Injectable, + NotFoundException, + InternalServerErrorException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Projects } from './project.model'; @@ -12,14 +16,19 @@ export class ProjectsService { @InjectRepository(Projects) private projectsRepository: Repository, @InjectRepository(ProjectPackages) - private projectPackagesRepository: Repository + private projectPackagesRepository: Repository, ) {} async getProjectsByUser(userId: string): Promise { - const projects = await this.projectsRepository.find({ where: { user_id: userId, is_deleted: false }, relations: ['projectPackages'] }); + const projects = await this.projectsRepository.find({ + where: { user_id: userId, is_deleted: false }, + relations: ['projectPackages'], + }); if (projects && projects.length > 0) { - projects.forEach(project => { - project.projectPackages = project.projectPackages.filter(pkg => !pkg.is_deleted); + projects.forEach((project) => { + project.projectPackages = project.projectPackages.filter( + (pkg) => !pkg.is_deleted, + ); }); } @@ -30,9 +39,14 @@ export class ProjectsService { } async getProjectById(projectId: string): Promise { - const project = await this.projectsRepository.findOne({ where: { id: projectId, is_deleted: false }, relations: ['projectPackages'] }); + const project = await this.projectsRepository.findOne({ + where: { id: projectId, is_deleted: false }, + relations: ['projectPackages'], + }); if (project) { - project.projectPackages = project.projectPackages.filter(pkg => !pkg.is_deleted); + project.projectPackages = project.projectPackages.filter( + (pkg) => !pkg.is_deleted, + ); } if (!project) { @@ -41,33 +55,38 @@ export class ProjectsService { return project; } - async upsertProject(upsertProjectInput: UpsertProjectInput, user_id: string): Promise { - const { project_id, project_name, path, project_packages } = upsertProjectInput; + async upsertProject( + upsertProjectInput: UpsertProjectInput, + user_id: string, + ): Promise { + const { project_id, project_name, path, project_packages } = + upsertProjectInput; let project; if (project_id) { // only extract the project match the user id - project = await this.projectsRepository.findOne({ where: { id: project_id, is_deleted: false, user_id: user_id } }); + project = await this.projectsRepository.findOne({ + where: { id: project_id, is_deleted: false, user_id: user_id }, + }); } - + if (project) { // Update existing project if (project_name) project.project_name = project_name; if (path) project.path = path; - } else { // Create a new project if it does not exist project = this.projectsRepository.create({ project_name, path, - user_id + user_id, }); project = await this.projectsRepository.save(project); } // Add new project packages to existing ones if (project_packages && project_packages.length > 0) { - const newPackages = project_packages.map(content => { + const newPackages = project_packages.map((content) => { return this.projectPackagesRepository.create({ project: project, content: content, @@ -77,16 +96,25 @@ export class ProjectsService { } // Return the updated or created project with all packages - return await this.projectsRepository.findOne({ where: { id: project.id, is_deleted: false }, relations: ['projectPackages'] }).then(project => { - if (project && project.projectPackages) { - project.projectPackages = project.projectPackages.filter(pkg => !pkg.is_deleted); - } - return project; - }); + return await this.projectsRepository + .findOne({ + where: { id: project.id, is_deleted: false }, + relations: ['projectPackages'], + }) + .then((project) => { + if (project && project.projectPackages) { + project.projectPackages = project.projectPackages.filter( + (pkg) => !pkg.is_deleted, + ); + } + return project; + }); } async deleteProject(projectId: string): Promise { - const project = await this.projectsRepository.findOne({ where: { id: projectId } }); + const project = await this.projectsRepository.findOne({ + where: { id: projectId }, + }); if (!project) { throw new NotFoundException(`Project with ID ${projectId} not found.`); } @@ -113,26 +141,41 @@ export class ProjectsService { } } - async removePackageFromProject(projectId: string, packageId: string): Promise { - const packageToRemove = await this.projectPackagesRepository.findOne({ where: { id: packageId, project: { id: projectId } } }); + async removePackageFromProject( + projectId: string, + packageId: string, + ): Promise { + const packageToRemove = await this.projectPackagesRepository.findOne({ + where: { id: packageId, project: { id: projectId } }, + }); if (!packageToRemove) { - throw new NotFoundException(`Package with ID ${packageId} not found for Project ID ${projectId}`); + throw new NotFoundException( + `Package with ID ${packageId} not found for Project ID ${projectId}`, + ); } - + packageToRemove.is_active = false; packageToRemove.is_deleted = true; await this.projectPackagesRepository.save(packageToRemove); - + return true; - } + } - async updateProjectPath(projectId: string, newPath: string): Promise { - const project = await this.projectsRepository.findOne({ where: { id: projectId, is_deleted: false }, relations: ['projectPackages'] }); + async updateProjectPath( + projectId: string, + newPath: string, + ): Promise { + const project = await this.projectsRepository.findOne({ + where: { id: projectId, is_deleted: false }, + relations: ['projectPackages'], + }); if (!project) { throw new NotFoundException(`Project with ID ${projectId} not found.`); } - - const result = await this.projectsRepository.update(projectId, { path: newPath }); + + const result = await this.projectsRepository.update(projectId, { + path: newPath, + }); return result.affected > 0; } } diff --git a/backend/src/user/user.resolver.ts b/backend/src/user/user.resolver.ts index acdd07b..f553066 100644 --- a/backend/src/user/user.resolver.ts +++ b/backend/src/user/user.resolver.ts @@ -36,7 +36,7 @@ export class UserResolver { //TODO use header authorization @Query(() => Boolean) - async logout(@GetAuthToken() token: string): Promise { + async logout(@GetAuthToken() token: string): Promise { return this.authService.logout(token); } } diff --git a/frontend/package.json b/frontend/package.json index 9ee171e..63bc4d7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "nextjs-ollama-local-ai", + "name": "codefox-web", "version": "0.1.0", "private": true, "scripts": { @@ -7,7 +7,8 @@ "start:dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint --fix", + "format": "prettier --write \"src/**/*.ts\"" }, "dependencies": { "@emoji-mart/data": "^1.2.1", @@ -59,7 +60,7 @@ "@types/react-dom": "^18.3.0", "@types/uuid": "^10.0.0", "autoprefixer": "^10.4.20", - "eslint": "^8.0.0", + "eslint": "8.57.1", "eslint-config-next": "14.2.13", "postcss": "^8.4.47", "tailwindcss": "^3.4.12", diff --git a/frontend/src/app/[id]/page.tsx b/frontend/src/app/[id]/page.tsx index f872429..52ab783 100644 --- a/frontend/src/app/[id]/page.tsx +++ b/frontend/src/app/[id]/page.tsx @@ -1,16 +1,16 @@ -"use client"; - -import { ChatLayout } from "@/components/chat/chat-layout"; -import { getSelectedModel } from "@/lib/model-helper"; -import { ChatOllama } from "@langchain/community/chat_models/ollama"; -import { AIMessage, HumanMessage } from "@langchain/core/messages"; -import { BytesOutputParser } from "@langchain/core/output_parsers"; -import { Attachment, ChatRequestOptions } from "ai"; -import { Message, useChat } from "ai/react"; -import React, { useEffect } from "react"; -import { toast } from "sonner"; -import { v4 as uuidv4 } from "uuid"; -import useChatStore from "../hooks/useChatStore"; +'use client'; + +import { ChatLayout } from '@/components/chat/chat-layout'; +import { getSelectedModel } from '@/lib/model-helper'; +import { ChatOllama } from '@langchain/community/chat_models/ollama'; +import { AIMessage, HumanMessage } from '@langchain/core/messages'; +import { BytesOutputParser } from '@langchain/core/output_parsers'; +import { Attachment, ChatRequestOptions } from 'ai'; +import { Message, useChat } from 'ai/react'; +import React, { useEffect } from 'react'; +import { toast } from 'sonner'; +import { v4 as uuidv4 } from 'uuid'; +import useChatStore from '../hooks/useChatStore'; export default function Page({ params }: { params: { id: string } }) { const { @@ -31,13 +31,12 @@ export default function Page({ params }: { params: { id: string } }) { }, onError: (error) => { setLoadingSubmit(false); - toast.error("An error occurred. Please try again."); + toast.error('An error occurred. Please try again.'); }, }); - const [chatId, setChatId] = React.useState(""); - const [selectedModel, setSelectedModel] = React.useState( - getSelectedModel() - ); + const [chatId, setChatId] = React.useState(''); + const [selectedModel, setSelectedModel] = + React.useState(getSelectedModel()); const [ollama, setOllama] = React.useState(); const env = process.env.NODE_ENV; const [loadingSubmit, setLoadingSubmit] = React.useState(false); @@ -46,9 +45,9 @@ export default function Page({ params }: { params: { id: string } }) { const setBase64Images = useChatStore((state) => state.setBase64Images); useEffect(() => { - if (env === "production") { + if (env === 'production') { const newOllama = new ChatOllama({ - baseUrl: process.env.NEXT_PUBLIC_OLLAMA_URL || "http://localhost:11434", + baseUrl: process.env.NEXT_PUBLIC_OLLAMA_URL || 'http://localhost:11434', model: selectedModel, }); setOllama(newOllama); @@ -66,7 +65,7 @@ export default function Page({ params }: { params: { id: string } }) { const addMessage = (Message: any) => { messages.push(Message); - window.dispatchEvent(new Event("storage")); + window.dispatchEvent(new Event('storage')); setMessages([...messages]); }; @@ -76,8 +75,8 @@ export default function Page({ params }: { params: { id: string } }) { ) => { e.preventDefault(); - addMessage({ role: "user", content: input, id: chatId }); - setInput(""); + addMessage({ role: 'user', content: input, id: chatId }); + setInput(''); if (ollama) { try { @@ -87,7 +86,7 @@ export default function Page({ params }: { params: { id: string } }) { .pipe(parser) .stream( (messages as Message[]).map((m) => - m.role == "user" + m.role == 'user' ? new HumanMessage(m.content) : new AIMessage(m.content) ) @@ -95,24 +94,24 @@ export default function Page({ params }: { params: { id: string } }) { const decoder = new TextDecoder(); - let responseMessage = ""; + let responseMessage = ''; for await (const chunk of stream) { const decodedChunk = decoder.decode(chunk); responseMessage += decodedChunk; setLoadingSubmit(false); setMessages([ ...messages, - { role: "assistant", content: responseMessage, id: chatId }, + { role: 'assistant', content: responseMessage, id: chatId }, ]); } - addMessage({ role: "assistant", content: responseMessage, id: chatId }); + addMessage({ role: 'assistant', content: responseMessage, id: chatId }); setMessages([...messages]); localStorage.setItem(`chat_${params.id}`, JSON.stringify(messages)); // Trigger the storage event to update the sidebar component - window.dispatchEvent(new Event("storage")); + window.dispatchEvent(new Event('storage')); } catch (error) { - toast.error("An error occurred. Please try again."); + toast.error('An error occurred. Please try again.'); setLoadingSubmit(false); } } @@ -125,11 +124,11 @@ export default function Page({ params }: { params: { id: string } }) { setMessages([...messages]); const attachments: Attachment[] = base64Images - ? base64Images.map((image) => ({ - contentType: 'image/base64', // Content type for base64 images - url: image, // The base64 image data - })) - : []; + ? base64Images.map((image) => ({ + contentType: 'image/base64', // Content type for base64 images + url: image, // The base64 image data + })) + : []; // Prepare the options object with additional body data, to pass the model. const requestOptions: ChatRequestOptions = { @@ -142,18 +141,18 @@ export default function Page({ params }: { params: { id: string } }) { data: { images: base64Images, }, - experimental_attachments: attachments + experimental_attachments: attachments, }), }; - if (env === "production" && selectedModel !== "REST API") { + if (env === 'production' && selectedModel !== 'REST API') { handleSubmitProduction(e); - setBase64Images(null) + setBase64Images(null); } else { // use the /api/chat route // Call the handleSubmit function with the options handleSubmit(e, requestOptions); - setBase64Images(null) + setBase64Images(null); } }; @@ -162,7 +161,7 @@ export default function Page({ params }: { params: { id: string } }) { if (!isLoading && !error && messages.length > 0) { localStorage.setItem(`chat_${params.id}`, JSON.stringify(messages)); // Trigger the storage event to update the sidebar component - window.dispatchEvent(new Event("storage")); + window.dispatchEvent(new Event('storage')); } }, [messages, chatId, isLoading, error]); diff --git a/frontend/src/app/api/chat/route.ts b/frontend/src/app/api/chat/route.ts index f96de16..fd70355 100644 --- a/frontend/src/app/api/chat/route.ts +++ b/frontend/src/app/api/chat/route.ts @@ -1,20 +1,27 @@ import { createOllama } from 'ollama-ai-provider'; -import { streamText, convertToCoreMessages, CoreMessage, UserContent } from 'ai'; +import { + streamText, + convertToCoreMessages, + CoreMessage, + UserContent, +} from 'ai'; -export const runtime = "edge"; -export const dynamic = "force-dynamic"; +export const runtime = 'edge'; +export const dynamic = 'force-dynamic'; export async function POST(req: Request) { // Destructure request data const { messages, selectedModel, data } = await req.json(); - const initialMessages = messages.slice(0, -1); - const currentMessage = messages[messages.length - 1]; + const initialMessages = messages.slice(0, -1); + const currentMessage = messages[messages.length - 1]; const ollama = createOllama({}); // Build message content array directly - const messageContent: UserContent = [{ type: 'text', text: currentMessage.content }]; + const messageContent: UserContent = [ + { type: 'text', text: currentMessage.content }, + ]; // Add images if they exist data?.images?.forEach((imageUrl: string) => { diff --git a/frontend/src/app/api/model/route.ts b/frontend/src/app/api/model/route.ts index 04e4c37..ccc2492 100644 --- a/frontend/src/app/api/model/route.ts +++ b/frontend/src/app/api/model/route.ts @@ -1,43 +1,47 @@ export async function POST(req: Request) { - const { name } = await req.json(); + const { name } = await req.json(); - const ollamaUrl = process.env.NEXT_PUBLIC_OLLAMA_URL || "http://localhost:11434"; + const ollamaUrl = + process.env.NEXT_PUBLIC_OLLAMA_URL || 'http://localhost:11434'; - const response = await fetch(ollamaUrl + "/api/pull", { - method: "POST", - body: JSON.stringify({ name }), - }); + const response = await fetch(ollamaUrl + '/api/pull', { + method: 'POST', + body: JSON.stringify({ name }), + }); - // Create a new ReadableStream from the response body - const stream = new ReadableStream({ - start(controller) { - if (!response.body) { - controller.close(); - return; - } - const reader = response.body.getReader(); + // Create a new ReadableStream from the response body + const stream = new ReadableStream({ + start(controller) { + if (!response.body) { + controller.close(); + return; + } + const reader = response.body.getReader(); - function pump() { - reader.read().then(({ done, value }) => { - if (done) { - controller.close(); - return; - } - // Enqueue the chunk of data to the controller - controller.enqueue(value); - pump(); - }).catch(error => { - console.error("Error reading response body:", error); - controller.error(error); - }); + function pump() { + reader + .read() + .then(({ done, value }) => { + if (done) { + controller.close(); + return; } - + // Enqueue the chunk of data to the controller + controller.enqueue(value); pump(); - } - }); + }) + .catch((error) => { + console.error('Error reading response body:', error); + controller.error(error); + }); + } + + pump(); + }, + }); - // Set response headers and return the stream - const headers = new Headers(response.headers); - headers.set("Content-Type", "application/json"); - return new Response(stream, { headers }); + // Set response headers and return the stream + const headers = new Headers(response.headers); + headers.set('Content-Type', 'application/json'); + return new Response(stream, { headers }); } diff --git a/frontend/src/app/api/tags/route.ts b/frontend/src/app/api/tags/route.ts index 15c926a..6056783 100644 --- a/frontend/src/app/api/tags/route.ts +++ b/frontend/src/app/api/tags/route.ts @@ -1,10 +1,9 @@ -export const dynamic = "force-dynamic"; +export const dynamic = 'force-dynamic'; export const revalidate = 0; export async function GET(req: Request) { - const OLLAMA_URL = process.env.NEXT_PUBLIC_OLLAMA_URL || "http://localhost:11434"; - const res = await fetch( - OLLAMA_URL + "/api/tags" - ); + const OLLAMA_URL = + process.env.NEXT_PUBLIC_OLLAMA_URL || 'http://localhost:11434'; + const res = await fetch(OLLAMA_URL + '/api/tags'); return new Response(res.body, res); } diff --git a/frontend/src/app/hooks/useChatStore.ts b/frontend/src/app/hooks/useChatStore.ts index 5683860..47523c5 100644 --- a/frontend/src/app/hooks/useChatStore.ts +++ b/frontend/src/app/hooks/useChatStore.ts @@ -1,6 +1,5 @@ - -import { CoreMessage } from "ai"; -import { create } from "zustand"; +import { CoreMessage } from 'ai'; +import { create } from 'zustand'; interface State { base64Images: string[] | null; @@ -9,21 +8,15 @@ interface State { interface Actions { setBase64Images: (base64Images: string[] | null) => void; - setMessages: ( - fn: ( - messages: CoreMessage[] - ) => CoreMessage[] - ) => void; + setMessages: (fn: (messages: CoreMessage[]) => CoreMessage[]) => void; } -const useChatStore = create()( - (set) => ({ - base64Images: null, - setBase64Images: (base64Images) => set({ base64Images }), +const useChatStore = create()((set) => ({ + base64Images: null, + setBase64Images: (base64Images) => set({ base64Images }), - messages: [], - setMessages: (fn) => set((state) => ({ messages: fn(state.messages) })), - }) -) + messages: [], + setMessages: (fn) => set((state) => ({ messages: fn(state.messages) })), +})); -export default useChatStore; \ No newline at end of file +export default useChatStore; diff --git a/frontend/src/app/hooks/useLocalStorageData.ts b/frontend/src/app/hooks/useLocalStorageData.ts index bff67b7..01e5420 100644 --- a/frontend/src/app/hooks/useLocalStorageData.ts +++ b/frontend/src/app/hooks/useLocalStorageData.ts @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; export const useLocalStorageData = (key: string, initialValue: any) => { const [data, setData] = useState(initialValue); - + useEffect(() => { const handleStorageChange = () => { const value = localStorage.getItem(key); diff --git a/frontend/src/app/hooks/useSpeechRecognition.ts b/frontend/src/app/hooks/useSpeechRecognition.ts index 52207fd..8874e41 100644 --- a/frontend/src/app/hooks/useSpeechRecognition.ts +++ b/frontend/src/app/hooks/useSpeechRecognition.ts @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect } from "react"; +import { useState, useRef, useEffect } from 'react'; interface SpeechRecognitionOptions { interimResults?: boolean; @@ -8,12 +8,12 @@ interface SpeechRecognitionOptions { const useSpeechToText = (options: SpeechRecognitionOptions = {}) => { const [isListening, setIsListening] = useState(false); - const [transcript, setTranscript] = useState(""); + const [transcript, setTranscript] = useState(''); const recognitionRef = useRef(null); useEffect(() => { - if (!("webkitSpeechRecognition" in window)) { - console.error("Web Speech API is not supported"); + if (!('webkitSpeechRecognition' in window)) { + console.error('Web Speech API is not supported'); return; } @@ -21,19 +21,19 @@ const useSpeechToText = (options: SpeechRecognitionOptions = {}) => { recognitionRef.current = recognition; recognition.interimResults = options.interimResults || true; - recognition.lang = options.lang || "en-US"; + recognition.lang = options.lang || 'en-US'; recognition.continuous = options.continuous || false; - if ("webkitSpeechGrammarList" in window) { + if ('webkitSpeechGrammarList' in window) { const grammar = - "#JSGF V1.0; grammar punctuation; public = . | , | ! | ; | : ;"; + '#JSGF V1.0; grammar punctuation; public = . | , | ! | ; | : ;'; const speechRecognitionList = new window.webkitSpeechGrammarList(); speechRecognitionList.addFromString(grammar, 1); recognition.grammars = speechRecognitionList; } recognition.onresult = (event: SpeechRecognitionEvent) => { - let text = ""; + let text = ''; for (let i = 0; i < event.results.length; i++) { text += event.results[i][0].transcript; @@ -49,7 +49,7 @@ const useSpeechToText = (options: SpeechRecognitionOptions = {}) => { recognition.onend = () => { setIsListening(false); - setTranscript(""); + setTranscript(''); }; return () => { diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 647ed68..7cba890 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,14 +1,14 @@ -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; -import "./globals.css"; -import { ThemeProvider } from "@/providers/theme-provider"; -import { Toaster } from "@/components/ui/sonner" +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; +import './globals.css'; +import { ThemeProvider } from '@/providers/theme-provider'; +import { Toaster } from '@/components/ui/sonner'; -const inter = Inter({ subsets: ["latin"] }); +const inter = Inter({ subsets: ['latin'] }); export const metadata: Metadata = { - title: "Ollama UI", - description: "Ollama chatbot web interface", + title: 'Ollama UI', + description: 'Ollama chatbot web interface', }; export const viewport = { @@ -16,7 +16,7 @@ export const viewport = { initialScale: 1, maximumScale: 1, userScalable: 1, -} +}; export default function RootLayout({ children, @@ -28,7 +28,7 @@ export default function RootLayout({ {children} - + diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 6c0bb0a..4daed61 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,26 +1,26 @@ -"use client"; +'use client'; -import { ChatLayout } from "@/components/chat/chat-layout"; -import { Button } from "@/components/ui/button"; +import { ChatLayout } from '@/components/chat/chat-layout'; +import { Button } from '@/components/ui/button'; import { Dialog, DialogDescription, DialogHeader, DialogTitle, DialogContent, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import UsernameForm from "@/components/username-form"; -import { getSelectedModel } from "@/lib/model-helper"; -import { ChatOllama } from "@langchain/community/chat_models/ollama"; -import { AIMessage, HumanMessage } from "@langchain/core/messages"; -import { BytesOutputParser } from "@langchain/core/output_parsers"; -import { Attachment, ChatRequestOptions } from "ai"; -import { Message, useChat } from "ai/react"; -import React, { useEffect, useRef, useState } from "react"; -import { toast } from "sonner"; -import { v4 as uuidv4 } from "uuid"; -import useChatStore from "./hooks/useChatStore"; +} from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import UsernameForm from '@/components/username-form'; +import { getSelectedModel } from '@/lib/model-helper'; +import { ChatOllama } from '@langchain/community/chat_models/ollama'; +import { AIMessage, HumanMessage } from '@langchain/core/messages'; +import { BytesOutputParser } from '@langchain/core/output_parsers'; +import { Attachment, ChatRequestOptions } from 'ai'; +import { Message, useChat } from 'ai/react'; +import React, { useEffect, useRef, useState } from 'react'; +import { toast } from 'sonner'; +import { v4 as uuidv4 } from 'uuid'; +import useChatStore from './hooks/useChatStore'; export default function Home() { const { @@ -42,13 +42,12 @@ export default function Home() { }, onError: (error) => { setLoadingSubmit(false); - toast.error("An error occurred. Please try again."); + toast.error('An error occurred. Please try again.'); }, }); - const [chatId, setChatId] = React.useState(""); - const [selectedModel, setSelectedModel] = React.useState( - getSelectedModel() - ); + const [chatId, setChatId] = React.useState(''); + const [selectedModel, setSelectedModel] = + React.useState(getSelectedModel()); const [open, setOpen] = React.useState(false); const [ollama, setOllama] = useState(); const env = process.env.NODE_ENV; @@ -60,7 +59,7 @@ export default function Home() { useEffect(() => { if (messages.length < 1) { // Generate a random id for the chat - console.log("Generating chat id"); + console.log('Generating chat id'); const id = uuidv4(); setChatId(id); } @@ -71,27 +70,27 @@ export default function Home() { // Save messages to local storage localStorage.setItem(`chat_${chatId}`, JSON.stringify(messages)); // Trigger the storage event to update the sidebar component - window.dispatchEvent(new Event("storage")); + window.dispatchEvent(new Event('storage')); } }, [chatId, isLoading, error]); useEffect(() => { - if (env === "production") { + if (env === 'production') { const newOllama = new ChatOllama({ - baseUrl: process.env.NEXT_PUBLIC_OLLAMA_URL || "http://localhost:11434", + baseUrl: process.env.NEXT_PUBLIC_OLLAMA_URL || 'http://localhost:11434', model: selectedModel, }); setOllama(newOllama); } - if (!localStorage.getItem("ollama_user")) { + if (!localStorage.getItem('ollama_user')) { setOpen(true); } }, [selectedModel]); const addMessage = (Message: Message) => { messages.push(Message); - window.dispatchEvent(new Event("storage")); + window.dispatchEvent(new Event('storage')); setMessages([...messages]); }; @@ -101,8 +100,8 @@ export default function Home() { ) => { e.preventDefault(); - addMessage({ role: "user", content: input, id: chatId }); - setInput(""); + addMessage({ role: 'user', content: input, id: chatId }); + setInput(''); if (ollama) { try { @@ -112,7 +111,7 @@ export default function Home() { .pipe(parser) .stream( (messages as Message[]).map((m) => - m.role == "user" + m.role == 'user' ? new HumanMessage(m.content) : new AIMessage(m.content) ) @@ -120,24 +119,24 @@ export default function Home() { const decoder = new TextDecoder(); - let responseMessage = ""; + let responseMessage = ''; for await (const chunk of stream) { const decodedChunk = decoder.decode(chunk); responseMessage += decodedChunk; setLoadingSubmit(false); setMessages([ ...messages, - { role: "assistant", content: responseMessage, id: chatId }, + { role: 'assistant', content: responseMessage, id: chatId }, ]); } - addMessage({ role: "assistant", content: responseMessage, id: chatId }); + addMessage({ role: 'assistant', content: responseMessage, id: chatId }); setMessages([...messages]); localStorage.setItem(`chat_${chatId}`, JSON.stringify(messages)); // Trigger the storage event to update the sidebar component - window.dispatchEvent(new Event("storage")); + window.dispatchEvent(new Event('storage')); } catch (error) { - toast.error("An error occurred. Please try again."); + toast.error('An error occurred. Please try again.'); setLoadingSubmit(false); } } @@ -150,11 +149,11 @@ export default function Home() { setMessages([...messages]); const attachments: Attachment[] = base64Images - ? base64Images.map((image) => ({ - contentType: 'image/base64', // Content type for base64 images - url: image, // The base64 image data - })) - : []; + ? base64Images.map((image) => ({ + contentType: 'image/base64', // Content type for base64 images + url: image, // The base64 image data + })) + : []; // Prepare the options object with additional body data, to pass the model. const requestOptions: ChatRequestOptions = { @@ -167,32 +166,31 @@ export default function Home() { data: { images: base64Images, }, - experimental_attachments: attachments + experimental_attachments: attachments, }), }; - messages.slice(0, -1) - + messages.slice(0, -1); - if (env === "production") { + if (env === 'production') { handleSubmitProduction(e); - setBase64Images(null) + setBase64Images(null); } else { // Call the handleSubmit function with the options handleSubmit(e, requestOptions); - setBase64Images(null) + setBase64Images(null); } }; - const onOpenChange = (isOpen: boolean) => { - const username = localStorage.getItem("ollama_user") - if (username) return setOpen(isOpen) + const onOpenChange = (isOpen: boolean) => { + const username = localStorage.getItem('ollama_user'); + if (username) return setOpen(isOpen); + + localStorage.setItem('ollama_user', 'Anonymous'); + window.dispatchEvent(new Event('storage')); + setOpen(isOpen); + }; - localStorage.setItem("ollama_user", "Anonymous") - window.dispatchEvent(new Event("storage")) - setOpen(isOpen) - } - return (
diff --git a/frontend/src/components/chat/chat-bottombar.tsx b/frontend/src/components/chat/chat-bottombar.tsx index 540bc1f..b5d8aab 100644 --- a/frontend/src/components/chat/chat-bottombar.tsx +++ b/frontend/src/components/chat/chat-bottombar.tsx @@ -1,18 +1,23 @@ -"use client"; - -import React, { useEffect } from "react"; -import { ChatProps } from "./chat"; -import Link from "next/link"; -import { cn } from "@/lib/utils"; -import { Button, buttonVariants } from "../ui/button"; -import TextareaAutosize from "react-textarea-autosize"; -import { motion, AnimatePresence } from "framer-motion"; -import { Cross2Icon, ImageIcon, PaperPlaneIcon, StopIcon } from "@radix-ui/react-icons"; -import { Mic, SendHorizonal } from "lucide-react"; -import useSpeechToText from "@/app/hooks/useSpeechRecognition"; -import MultiImagePicker from "../image-embedder"; -import useChatStore from "@/app/hooks/useChatStore"; -import Image from "next/image"; +'use client'; + +import React, { useEffect } from 'react'; +import { ChatProps } from './chat'; +import Link from 'next/link'; +import { cn } from '@/lib/utils'; +import { Button, buttonVariants } from '../ui/button'; +import TextareaAutosize from 'react-textarea-autosize'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + Cross2Icon, + ImageIcon, + PaperPlaneIcon, + StopIcon, +} from '@radix-ui/react-icons'; +import { Mic, SendHorizonal } from 'lucide-react'; +import useSpeechToText from '@/app/hooks/useSpeechRecognition'; +import MultiImagePicker from '../image-embedder'; +import useChatStore from '@/app/hooks/useChatStore'; +import Image from 'next/image'; export default function ChatBottombar({ messages, @@ -41,16 +46,16 @@ export default function ChatBottombar({ checkScreenWidth(); // Event listener for screen width changes - window.addEventListener("resize", checkScreenWidth); + window.addEventListener('resize', checkScreenWidth); // Cleanup the event listener on component unmount return () => { - window.removeEventListener("resize", checkScreenWidth); + window.removeEventListener('resize', checkScreenWidth); }; }, []); const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === "Enter" && !e.shiftKey) { + if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSubmit(e as unknown as React.FormEvent); } @@ -64,7 +69,7 @@ export default function ChatBottombar({ }; const stopVoiceInput = () => { - setInput && setInput(transcript.length ? transcript : ""); + setInput && setInput(transcript.length ? transcript : ''); stopListening(); }; @@ -88,26 +93,29 @@ export default function ChatBottombar({
-
+
- +
@@ -175,29 +183,38 @@ export default function ChatBottombar({
)} -
{base64Images && (
{base64Images.map((image, index) => { return ( -
+
- {""} + className="h-auto rounded-md w-auto max-w-[100px] max-h-[100px]" + alt={''} + />
- ) + ); })}
)} diff --git a/frontend/src/components/chat/chat-layout.tsx b/frontend/src/components/chat/chat-layout.tsx index 2762d7f..3a38b27 100644 --- a/frontend/src/components/chat/chat-layout.tsx +++ b/frontend/src/components/chat/chat-layout.tsx @@ -1,17 +1,17 @@ -"use client"; +'use client'; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState } from 'react'; import { ResizableHandle, ResizablePanel, ResizablePanelGroup, -} from "@/components/ui/resizable"; -import { cn } from "@/lib/utils"; -import { Sidebar } from "../sidebar"; -import { Message, useChat } from "ai/react"; -import Chat, { ChatProps } from "./chat"; -import ChatList from "./chat-list"; -import { HamburgerMenuIcon } from "@radix-ui/react-icons"; +} from '@/components/ui/resizable'; +import { cn } from '@/lib/utils'; +import { Sidebar } from '../sidebar'; +import { Message, useChat } from 'ai/react'; +import Chat, { ChatProps } from './chat'; +import ChatList from './chat-list'; +import { HamburgerMenuIcon } from '@radix-ui/react-icons'; interface ChatLayoutProps { defaultLayout: number[] | undefined; @@ -53,11 +53,11 @@ export function ChatLayout({ checkScreenWidth(); // Event listener for screen width changes - window.addEventListener("resize", checkScreenWidth); + window.addEventListener('resize', checkScreenWidth); // Cleanup the event listener on component unmount return () => { - window.removeEventListener("resize", checkScreenWidth); + window.removeEventListener('resize', checkScreenWidth); }; }, []); @@ -91,8 +91,8 @@ export function ChatLayout({ }} className={cn( isCollapsed - ? "min-w-[50px] md:min-w-[70px] transition-all duration-300 ease-in-out" - : "hidden md:block" + ? 'min-w-[50px] md:min-w-[70px] transition-all duration-300 ease-in-out' + : 'hidden md:block' )} > - + (null); - const [name, setName] = React.useState(""); + const [name, setName] = React.useState(''); const [localStorageIsLoading, setLocalStorageIsLoading] = React.useState(true); const [initialQuestions, setInitialQuestions] = React.useState([]); const scrollToBottom = () => { - bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "end" }); + bottomRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }); }; useEffect(() => { @@ -38,7 +38,7 @@ export default function ChatList({ }, [messages]); useEffect(() => { - const username = localStorage.getItem("ollama_user"); + const username = localStorage.getItem('ollama_user'); if (username) { setName(username); setLocalStorageIsLoading(false); @@ -55,8 +55,8 @@ export default function ChatList({ .slice(0, questionCount) .map((message) => { return { - id: "1", - role: "user", + id: '1', + role: 'user', content: message.content, }; }) @@ -73,7 +73,7 @@ export default function ChatList({ setTimeout(() => { formRef.current?.dispatchEvent( - new Event("submit", { + new Event('submit', { cancelable: true, bubbles: true, }) @@ -81,7 +81,7 @@ export default function ChatList({ }, 1); }; - messages.map((m) => console.log(m.experimental_attachments)) + messages.map((m) => console.log(m.experimental_attachments)); if (messages.length === 0) { return ( @@ -115,7 +115,7 @@ export default function ChatList({ transition={{ opacity: { duration: 0.1, delay }, scale: { duration: 0.1, delay }, - y: { type: "spring", stiffness: 100, damping: 10, delay }, + y: { type: 'spring', stiffness: 100, damping: 10, delay }, }} key={message.content} > @@ -153,30 +153,35 @@ export default function ChatList({ transition={{ opacity: { duration: 0.1 }, layout: { - type: "spring", + type: 'spring', bounce: 0.3, duration: messages.indexOf(message) * 0.05 + 0.2, }, }} className={cn( - "flex flex-col gap-2 p-4 whitespace-pre-wrap", - message.role === "user" ? "items-end" : "items-start" + 'flex flex-col gap-2 p-4 whitespace-pre-wrap', + message.role === 'user' ? 'items-end' : 'items-start' )} >
- {message.role === "user" && ( + {message.role === 'user' && (
- {message.experimental_attachments?.filter(attachment => attachment.contentType?.startsWith('image/'),).map((attachment, index) => ( - attached image - ))} + {message.experimental_attachments + ?.filter((attachment) => + attachment.contentType?.startsWith('image/') + ) + .map((attachment, index) => ( + attached image + ))}

{message.content}

@@ -194,7 +199,7 @@ export default function ChatList({
)} - {message.role === "assistant" && ( + {message.role === 'assistant' && (
{/* Check if the message content contains a code block */} - {message.content.split("```").map((part, index) => { + {message.content.split('```').map((part, index) => { if (index % 2 === 0) { return ( diff --git a/frontend/src/components/chat/chat-topbar.tsx b/frontend/src/components/chat/chat-topbar.tsx index a8701bb..1cc1896 100644 --- a/frontend/src/components/chat/chat-topbar.tsx +++ b/frontend/src/components/chat/chat-topbar.tsx @@ -1,11 +1,11 @@ -"use client"; +'use client'; -import React, { useEffect } from "react"; +import React, { useEffect } from 'react'; import { Popover, PopoverContent, PopoverTrigger, -} from "@/components/ui/popover"; +} from '@/components/ui/popover'; import { Sheet, SheetContent, @@ -13,13 +13,13 @@ import { SheetHeader, SheetTitle, SheetTrigger, -} from "@/components/ui/sheet"; +} from '@/components/ui/sheet'; -import { Button } from "../ui/button"; -import { CaretSortIcon, HamburgerMenuIcon } from "@radix-ui/react-icons"; -import { Sidebar } from "../sidebar"; -import { Message } from "ai/react"; -import { getSelectedModel } from "@/lib/model-helper"; +import { Button } from '../ui/button'; +import { CaretSortIcon, HamburgerMenuIcon } from '@radix-ui/react-icons'; +import { Sidebar } from '../sidebar'; +import { Message } from 'ai/react'; +import { getSelectedModel } from '@/lib/model-helper'; interface ChatTopbarProps { setSelectedModel: React.Dispatch>; @@ -34,7 +34,7 @@ export default function ChatTopbar({ isLoading, chatId, messages, - setMessages + setMessages, }: ChatTopbarProps) { const [models, setModels] = React.useState([]); const [open, setOpen] = React.useState(false); @@ -47,19 +47,20 @@ export default function ChatTopbar({ const env = process.env.NODE_ENV; const fetchModels = async () => { - if (env === "production") { - const fetchedModels = await fetch(process.env.NEXT_PUBLIC_OLLAMA_URL + "/api/tags"); + if (env === 'production') { + const fetchedModels = await fetch( + process.env.NEXT_PUBLIC_OLLAMA_URL + '/api/tags' + ); const json = await fetchedModels.json(); - const apiModels = json.models.map((model : any) => model.name); + const apiModels = json.models.map((model: any) => model.name); setModels([...apiModels]); - } - else { - const fetchedModels = await fetch("/api/tags") + } else { + const fetchedModels = await fetch('/api/tags'); const json = await fetchedModels.json(); - const apiModels = json.models.map((model : any) => model.name); + const apiModels = json.models.map((model: any) => model.name); setModels([...apiModels]); - } - } + } + }; fetchModels(); }, []); @@ -67,13 +68,13 @@ export default function ChatTopbar({ setCurrentModel(model); setSelectedModel(model); if (typeof window !== 'undefined') { - localStorage.setItem("selectedModel", model); + localStorage.setItem('selectedModel', model); } setOpen(false); }; const handleCloseSidebar = () => { - setSheetOpen(false); // Close the sidebar + setSheetOpen(false); // Close the sidebar }; return ( @@ -84,12 +85,12 @@ export default function ChatTopbar({ @@ -103,7 +104,7 @@ export default function ChatTopbar({ aria-expanded={open} className="w-[300px] justify-between" > - {currentModel || "Select model"} + {currentModel || 'Select model'} diff --git a/frontend/src/components/chat/chat.tsx b/frontend/src/components/chat/chat.tsx index dd789fe..c34e228 100644 --- a/frontend/src/components/chat/chat.tsx +++ b/frontend/src/components/chat/chat.tsx @@ -1,10 +1,10 @@ -import React from "react"; -import ChatTopbar from "./chat-topbar"; -import ChatList from "./chat-list"; -import ChatBottombar from "./chat-bottombar"; -import { Message, useChat } from "ai/react"; -import { ChatRequestOptions } from "ai"; -import { v4 as uuidv4 } from "uuid"; +import React from 'react'; +import ChatTopbar from './chat-topbar'; +import ChatList from './chat-list'; +import ChatBottombar from './chat-bottombar'; +import { Message, useChat } from 'ai/react'; +import { ChatRequestOptions } from 'ai'; +import { v4 as uuidv4 } from 'uuid'; export interface ChatProps { chatId?: string; @@ -40,7 +40,7 @@ export default function Chat({ formRef, isMobile, setInput, - setMessages + setMessages, }: ChatProps) { return (
diff --git a/frontend/src/components/code-display-block.tsx b/frontend/src/components/code-display-block.tsx index 02fe71b..452ab4d 100644 --- a/frontend/src/components/code-display-block.tsx +++ b/frontend/src/components/code-display-block.tsx @@ -1,10 +1,10 @@ -"use client"; -import { CheckIcon, CopyIcon } from "@radix-ui/react-icons"; -import React from "react"; -import { CodeBlock, dracula, github } from "react-code-blocks"; -import { Button } from "./ui/button"; -import { toast } from "sonner"; -import { useTheme } from "next-themes"; +'use client'; +import { CheckIcon, CopyIcon } from '@radix-ui/react-icons'; +import React from 'react'; +import { CodeBlock, dracula, github } from 'react-code-blocks'; +import { Button } from './ui/button'; +import { toast } from 'sonner'; +import { useTheme } from 'next-themes'; interface ButtonCodeblockProps { code: string; @@ -18,7 +18,7 @@ export default function CodeDisplayBlock({ code, lang }: ButtonCodeblockProps) { const copyToClipboard = () => { navigator.clipboard.writeText(code); setisCopied(true); - toast.success("Code copied to clipboard!"); + toast.success('Code copied to clipboard!'); setTimeout(() => { setisCopied(false); }, 1500); @@ -40,14 +40,14 @@ export default function CodeDisplayBlock({ code, lang }: ButtonCodeblockProps) {
); diff --git a/frontend/src/components/edit-username-form.tsx b/frontend/src/components/edit-username-form.tsx index a857323..d4bd773 100644 --- a/frontend/src/components/edit-username-form.tsx +++ b/frontend/src/components/edit-username-form.tsx @@ -1,9 +1,9 @@ -"use client"; +'use client'; -import { set, z } from "zod"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; -import { Button } from "@/components/ui/button"; +import { set, z } from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { Button } from '@/components/ui/button'; import { Form, FormControl, @@ -12,16 +12,15 @@ import { FormItem, FormLabel, FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import React, { useEffect, useState } from "react"; -import { ModeToggle } from "./mode-toggle"; -import { toast } from "sonner" - +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import React, { useEffect, useState } from 'react'; +import { ModeToggle } from './mode-toggle'; +import { toast } from 'sonner'; const formSchema = z.object({ username: z.string().min(2, { - message: "Name must be at least 2 characters.", + message: 'Name must be at least 2 characters.', }), }); @@ -30,37 +29,37 @@ interface EditUsernameFormProps { } export default function EditUsernameForm({ setOpen }: EditUsernameFormProps) { - const [name, setName] = useState(""); + const [name, setName] = useState(''); useEffect(() => { - setName(localStorage.getItem("ollama_user") || "Anonymous"); + setName(localStorage.getItem('ollama_user') || 'Anonymous'); }, []); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { - username: "", + username: '', }, }); function onSubmit(values: z.infer) { - localStorage.setItem("ollama_user", values.username); - window.dispatchEvent(new Event("storage")); - toast.success("Name updated successfully"); + localStorage.setItem('ollama_user', values.username); + window.dispatchEvent(new Event('storage')); + toast.success('Name updated successfully'); } const handleChange = (e: React.ChangeEvent) => { e.preventDefault(); - form.setValue("username", e.currentTarget.value); + form.setValue('username', e.currentTarget.value); setName(e.currentTarget.value); }; return (
-
- Theme +
+ Theme -
+
handleChange(e)} /> - +
diff --git a/frontend/src/components/emoji-picker.tsx b/frontend/src/components/emoji-picker.tsx index 66483e7..d7785c9 100644 --- a/frontend/src/components/emoji-picker.tsx +++ b/frontend/src/components/emoji-picker.tsx @@ -1,38 +1,33 @@ -'use client' +'use client'; import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover" -import { SmileIcon } from "lucide-react"; + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; +import { SmileIcon } from 'lucide-react'; import Picker from '@emoji-mart/react'; -import data from "@emoji-mart/data" +import data from '@emoji-mart/data'; interface EmojiPickerProps { - onChange: (value: string) => void; + onChange: (value: string) => void; } - -export const EmojiPicker = ({ - onChange -}: EmojiPickerProps) => { - +export const EmojiPicker = ({ onChange }: EmojiPickerProps) => { return ( - - - - - onChange(emoji.native)} - /> - + + + + + onChange(emoji.native)} + /> + - ) -} + ); +}; diff --git a/frontend/src/components/image-embedder.tsx b/frontend/src/components/image-embedder.tsx index 2ad273b..244776d 100644 --- a/frontend/src/components/image-embedder.tsx +++ b/frontend/src/components/image-embedder.tsx @@ -1,32 +1,37 @@ -"use client"; +'use client'; -import React, { useCallback } from "react"; -import { useDropzone } from "react-dropzone"; -import { Button } from "./ui/button"; -import { ImageIcon } from "lucide-react"; +import React, { useCallback } from 'react'; +import { useDropzone } from 'react-dropzone'; +import { Button } from './ui/button'; +import { ImageIcon } from 'lucide-react'; interface MultiImagePickerProps { onImagesPick: (base64Images: string[]) => void; - disabled: boolean + disabled: boolean; } -const MultiImagePicker: React.FC = ({ onImagesPick, disabled }) => { +const MultiImagePicker: React.FC = ({ + onImagesPick, + disabled, +}) => { const convertToBase64 = (file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => resolve(reader.result as string); - reader.onerror = error => reject(error); + reader.onerror = (error) => reject(error); }); }; const onDrop = useCallback( async (acceptedFiles: File[]) => { try { - const base64Images = await Promise.all(acceptedFiles.map(convertToBase64)); + const base64Images = await Promise.all( + acceptedFiles.map(convertToBase64) + ); onImagesPick(base64Images); } catch (error) { - console.error("Error converting images to base64:", error); + console.error('Error converting images to base64:', error); } }, [onImagesPick] @@ -35,7 +40,7 @@ const MultiImagePicker: React.FC = ({ onImagesPick, disab const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, accept: { - 'image/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp'] + 'image/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp'], }, multiple: true, // Allow multiple file selection maxSize: 10485760, // 10 MB per file @@ -44,7 +49,13 @@ const MultiImagePicker: React.FC = ({ onImagesPick, disab return (
- @@ -52,4 +63,4 @@ const MultiImagePicker: React.FC = ({ onImagesPick, disab ); }; -export default MultiImagePicker; \ No newline at end of file +export default MultiImagePicker; diff --git a/frontend/src/components/mode-toggle.tsx b/frontend/src/components/mode-toggle.tsx index dbabf09..e5c19b3 100644 --- a/frontend/src/components/mode-toggle.tsx +++ b/frontend/src/components/mode-toggle.tsx @@ -1,16 +1,16 @@ -"use client"; +'use client'; -import * as React from "react"; -import { ChevronDownIcon, MoonIcon, SunIcon } from "@radix-ui/react-icons"; -import { useTheme } from "next-themes"; +import * as React from 'react'; +import { ChevronDownIcon, MoonIcon, SunIcon } from '@radix-ui/react-icons'; +import { useTheme } from 'next-themes'; -import { Button } from "@/components/ui/button"; +import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; +} from '@/components/ui/dropdown-menu'; export function ModeToggle() { const { setTheme, theme } = useTheme(); @@ -19,26 +19,26 @@ export function ModeToggle() {
)} - {theme === "dark" && ( + {theme === 'dark' && (
-

Dark mode

- -
+

Dark mode

+ +
)} Toggle theme - setTheme("light")}> + setTheme('light')}> Light mode - setTheme("dark")}> + setTheme('dark')}> Dark mode diff --git a/frontend/src/components/pull-model-form.tsx b/frontend/src/components/pull-model-form.tsx index 56d679e..30d9d14 100644 --- a/frontend/src/components/pull-model-form.tsx +++ b/frontend/src/components/pull-model-form.tsx @@ -1,31 +1,31 @@ -"use client"; +'use client'; -import React, { useState } from "react"; +import React, { useState } from 'react'; import { Form, FormField, FormItem, FormLabel, FormMessage, -} from "@/components/ui/form"; -import { Button } from "./ui/button"; -import { z } from "zod"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { toast } from "sonner"; -import { Loader2Icon } from "lucide-react"; -import { Input } from "./ui/input"; -import { useRouter } from "next/navigation"; +} from '@/components/ui/form'; +import { Button } from './ui/button'; +import { z } from 'zod'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { toast } from 'sonner'; +import { Loader2Icon } from 'lucide-react'; +import { Input } from './ui/input'; +import { useRouter } from 'next/navigation'; const formSchema = z.object({ name: z.string().min(1, { - message: "Please select a model to pull", + message: 'Please select a model to pull', }), }); export default function PullModelForm() { const [isDownloading, setIsDownloading] = useState(false); - const [name, setName] = useState(""); + const [name, setName] = useState(''); const router = useRouter(); const env = process.env.NODE_ENV; @@ -39,96 +39,99 @@ export default function PullModelForm() { setIsDownloading(true); // Send the model name to the server - if (env === "production") { + if (env === 'production') { // Make a post request to localhost const pullModel = async () => { - const response = await fetch(process.env.NEXT_PUBLIC_OLLAMA_URL + "/api/pull", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(data), - }); + const response = await fetch( + process.env.NEXT_PUBLIC_OLLAMA_URL + '/api/pull', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + } + ); const json = await response.json(); if (json.error) { - toast.error("Error: " + json.error); + toast.error('Error: ' + json.error); setIsDownloading(false); return; - } else if (json.status === "success") { - toast.success("Model pulled successfully"); + } else if (json.status === 'success') { + toast.success('Model pulled successfully'); setIsDownloading(false); return; } - } + }; pullModel(); } else { - fetch("/api/model", { - method: "POST", + fetch('/api/model', { + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, body: JSON.stringify(data), }) .then((response) => { // Check if response is successful if (!response.ok) { - throw new Error("Network response was not ok"); + throw new Error('Network response was not ok'); } if (!response.body) { - throw new Error("Something went wrong"); + throw new Error('Something went wrong'); } // Create a new ReadableStream from the response body const reader = response.body.getReader(); - + // Read the data in chunks reader.read().then(function processText({ done, value }) { if (done) { setIsDownloading(false); return; } - + // Convert the chunk of data to a string const text = new TextDecoder().decode(value); - + // Split the text into individual JSON objects - const jsonObjects = text.trim().split("\n"); - + const jsonObjects = text.trim().split('\n'); + jsonObjects.forEach((jsonObject) => { try { const responseJson = JSON.parse(jsonObject); if (responseJson.error) { // Display an error toast if the response contains an error - toast.error("Error: " + responseJson.error); + toast.error('Error: ' + responseJson.error); setIsDownloading(false); return; - } else if (responseJson.status === "success") { + } else if (responseJson.status === 'success') { // Display a success toast if the response status is success - toast.success("Model pulled successfully"); + toast.success('Model pulled successfully'); setIsDownloading(false); return; } } catch (error) { - toast.error("Error parsing JSON"); + toast.error('Error parsing JSON'); setIsDownloading(false); return; } }); - + // Continue reading the next chunk reader.read().then(processText); }); }) .catch((error) => { setIsDownloading(false); - console.error("Error pulling model:", error); - toast.error("Error pulling model"); + console.error('Error pulling model:', error); + toast.error('Error pulling model'); }); } } const handleChange = (e: React.ChangeEvent) => { e.preventDefault(); - form.setValue("name", e.currentTarget.value); + form.setValue('name', e.currentTarget.value); setName(e.currentTarget.value); }; @@ -149,14 +152,14 @@ export default function PullModelForm() { onChange={(e) => handleChange(e)} />

- Check the{" "} + Check the{' '} library - {" "} + {' '} for a list of available models.

@@ -171,13 +174,13 @@ export default function PullModelForm() { Pulling model...
) : ( - "Pull model" + 'Pull model' )}

{isDownloading - ? "This may take a while. You can safely close this modal and continue using the app" - : "Pressing the button will download the specified model to your device."} + ? 'This may take a while. You can safely close this modal and continue using the app' + : 'Pressing the button will download the specified model to your device.'}

diff --git a/frontend/src/components/pull-model.tsx b/frontend/src/components/pull-model.tsx index b808f85..fc41c91 100644 --- a/frontend/src/components/pull-model.tsx +++ b/frontend/src/components/pull-model.tsx @@ -1,5 +1,5 @@ -import React, { useEffect } from "react"; -import { Button } from "./ui/button"; +import React, { useEffect } from 'react'; +import { Button } from './ui/button'; import { Dialog, DialogContent, @@ -7,20 +7,19 @@ import { DialogHeader, DialogTitle, DialogTrigger, -} from "./ui/dialog"; +} from './ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select"; +} from '@/components/ui/select'; -import { DownloadIcon } from "@radix-ui/react-icons"; -import PullModelForm from "./pull-model-form"; +import { DownloadIcon } from '@radix-ui/react-icons'; +import PullModelForm from './pull-model-form'; export default function PullModel() { - return ( @@ -30,8 +29,8 @@ export default function PullModel() {
- Pull Model - + Pull Model +
); diff --git a/frontend/src/components/sidebar-skeleton.tsx b/frontend/src/components/sidebar-skeleton.tsx index 2ad3472..3bdc68f 100644 --- a/frontend/src/components/sidebar-skeleton.tsx +++ b/frontend/src/components/sidebar-skeleton.tsx @@ -1,4 +1,4 @@ -import { Skeleton } from "@/components/ui/skeleton"; +import { Skeleton } from '@/components/ui/skeleton'; export default function SidebarSkeleton() { return ( @@ -27,7 +27,6 @@ export default function SidebarSkeleton() { - ); } diff --git a/frontend/src/components/sidebar.tsx b/frontend/src/components/sidebar.tsx index 4b27be5..4945ac8 100644 --- a/frontend/src/components/sidebar.tsx +++ b/frontend/src/components/sidebar.tsx @@ -1,18 +1,18 @@ -"use client"; +'use client'; -import Link from "next/link"; -import { MoreHorizontal, SquarePen, Trash2 } from "lucide-react"; -import { cn } from "@/lib/utils"; -import { Button, buttonVariants } from "@/components/ui/button"; -import { Message } from "ai/react"; -import Image from "next/image"; -import { useEffect, useState } from "react"; -import SidebarSkeleton from "./sidebar-skeleton"; -import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"; -import UserSettings from "./user-settings"; -import { useLocalStorageData } from "@/app/hooks/useLocalStorageData"; -import { ScrollArea, Scrollbar } from "@radix-ui/react-scroll-area"; -import PullModel from "./pull-model"; +import Link from 'next/link'; +import { MoreHorizontal, SquarePen, Trash2 } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { Button, buttonVariants } from '@/components/ui/button'; +import { Message } from 'ai/react'; +import Image from 'next/image'; +import { useEffect, useState } from 'react'; +import SidebarSkeleton from './sidebar-skeleton'; +import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'; +import UserSettings from './user-settings'; +import { useLocalStorageData } from '@/app/hooks/useLocalStorageData'; +import { ScrollArea, Scrollbar } from '@radix-ui/react-scroll-area'; +import PullModel from './pull-model'; import { Dialog, DialogContent, @@ -20,14 +20,14 @@ import { DialogHeader, DialogTitle, DialogTrigger, -} from "./ui/dialog"; +} from './ui/dialog'; import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, -} from "./ui/dropdown-menu"; -import { TrashIcon } from "@radix-ui/react-icons"; -import { useRouter } from "next/navigation"; +} from './ui/dropdown-menu'; +import { TrashIcon } from '@radix-ui/react-icons'; +import { useRouter } from 'next/navigation'; interface SidebarProps { isCollapsed: boolean; @@ -45,12 +45,12 @@ export function Sidebar({ isMobile, chatId, setMessages, - closeSidebar + closeSidebar, }: SidebarProps) { const [localChats, setLocalChats] = useState< { chatId: string; messages: Message[] }[] >([]); - const localChatss = useLocalStorageData("chat_", []); + const localChatss = useLocalStorageData('chat_', []); const [selectedChatId, setSselectedChatId] = useState(null); const [isLoading, setIsLoading] = useState(true); const router = useRouter(); @@ -64,9 +64,9 @@ export function Sidebar({ const handleStorageChange = () => { setLocalChats(getLocalstorageChats()); }; - window.addEventListener("storage", handleStorageChange); + window.addEventListener('storage', handleStorageChange); return () => { - window.removeEventListener("storage", handleStorageChange); + window.removeEventListener('storage', handleStorageChange); }; }, []); @@ -75,7 +75,7 @@ export function Sidebar({ messages: Message[]; }[] => { const chats = Object.keys(localStorage).filter((key) => - key.startsWith("chat_") + key.startsWith('chat_') ); if (chats.length === 0) { @@ -87,7 +87,7 @@ export function Sidebar({ const item = localStorage.getItem(chat); return item ? { chatId: chat, messages: JSON.parse(item) } - : { chatId: "", messages: [] }; + : { chatId: '', messages: [] }; }); // Sort chats by the createdAt date of the first message of each chat @@ -114,7 +114,7 @@ export function Sidebar({