diff --git a/apps/backend/prisma/migrations/20260612230536_add_pgvector/migration.sql b/apps/backend/prisma/migrations/20260612230536_add_pgvector/migration.sql new file mode 100644 index 0000000..30d07b3 --- /dev/null +++ b/apps/backend/prisma/migrations/20260612230536_add_pgvector/migration.sql @@ -0,0 +1,15 @@ +-- CreateExtension +CREATE EXTENSION IF NOT EXISTS "vector"; + +-- CreateTable +CREATE TABLE "NoteEmbedding" ( + "id" TEXT NOT NULL, + "content" TEXT NOT NULL, + "embedding" vector(768), + "noteId" TEXT NOT NULL, + + CONSTRAINT "NoteEmbedding_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "NoteEmbedding" ADD CONSTRAINT "NoteEmbedding_noteId_fkey" FOREIGN KEY ("noteId") REFERENCES "Note"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 9badf47..018bd88 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -1,9 +1,11 @@ generator client { - provider = "prisma-client-js" + provider = "prisma-client-js" + previewFeatures = ["postgresqlExtensions"] } datasource db { - provider = "postgresql" + provider = "postgresql" + extensions = [vector] } model User { @@ -43,22 +45,31 @@ model Project { } model Note { - id String @id @default(uuid()) - title String - content String? - tags String[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - projectId String? - project Project? @relation(fields: [projectId], references: [id], onDelete: SetNull) + id String @id @default(uuid()) + title String + content String? + tags String[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + projectId String? + project Project? @relation(fields: [projectId], references: [id], onDelete: SetNull) searchVector Unsupported("tsvector")? - versions NoteVersion[] + versions NoteVersion[] + embeddings NoteEmbedding[] @@index([searchVector], type: Gin) } +model NoteEmbedding { + id String @id @default(uuid()) + content String + embedding Unsupported("vector(768)")? + noteId String + note Note @relation(fields: [noteId], references: [id], onDelete: Cascade) +} + model NoteVersion { id String @id @default(uuid()) title String diff --git a/apps/backend/src/ai/ai.controller.ts b/apps/backend/src/ai/ai.controller.ts index f4f4c17..522ebce 100644 --- a/apps/backend/src/ai/ai.controller.ts +++ b/apps/backend/src/ai/ai.controller.ts @@ -30,6 +30,28 @@ export class AiController { return this.aiService.streamChat(req.user.id, body, res); } + @Post('analyze-code') + async analyzeCode(@Body() body: { code: string; model?: string }) { + return this.aiService.analyzeCode(body.code, body.model); + } + + @Post('debug') + async debugCode( + @Body() body: { code: string; errorMessage: string; model?: string }, + ) { + return this.aiService.debugCode(body.code, body.errorMessage, body.model); + } + + @Post('generate-docs') + async generateDocs(@Body() body: { code: string; model?: string }) { + return this.aiService.generateDocs(body.code, body.model); + } + + @Post('embeddings') + async generateEmbeddings(@Body() body: { text: string; model?: string }) { + return this.aiService.generateEmbeddings(body.text, body.model); + } + @Get('conversations') getConversations(@Req() req: RequestWithUser) { return this.aiService.getConversations(req.user.id); @@ -39,4 +61,9 @@ export class AiController { getMessages(@Req() req: RequestWithUser, @Param('id') id: string) { return this.aiService.getMessages(id, req.user.id); } + + @Get('models') + getModels() { + return this.aiService.getModels(); + } } diff --git a/apps/backend/src/ai/ai.service.ts b/apps/backend/src/ai/ai.service.ts index 7ce7d05..d141448 100644 --- a/apps/backend/src/ai/ai.service.ts +++ b/apps/backend/src/ai/ai.service.ts @@ -48,11 +48,40 @@ export class AiService { orderBy: { createdAt: 'asc' }, }); - const messages = history.map((m) => ({ role: m.role, content: m.content })); + // Context trimming: Keep only the last 10 messages to prevent token overflow + const recentHistory = history.slice(-10); + + const messages = recentHistory.map((m) => ({ + role: m.role, + content: m.content, + })); + + // RAG INJECTION + let contextTexts = ''; + try { + const { embedding } = await this.generateEmbeddings(message); + const embeddingString = `[${embedding.join(',')}]`; + + const similarChunks = await this.prisma.$queryRaw<{ content: string }[]>` + SELECT ne.content + FROM "NoteEmbedding" ne + JOIN "Note" n ON n.id = ne."noteId" + WHERE n."userId" = ${userId} + ORDER BY ne.embedding <=> ${embeddingString}::vector + LIMIT 3 + `; + contextTexts = similarChunks.map((c) => c.content).join('\n---\n'); + } catch (err) { + console.error('Failed to fetch RAG context:', err); + } + messages.unshift({ role: 'system', - content: - 'You are DevFlow AI, a highly skilled and helpful senior software engineer. Provide clear, concise answers with accurate code examples when needed. Format your code blocks with the correct language identifiers.', + content: `You are a helpful AI coding assistant named DevFlow AI. +Here is some context from the user's notes that might be relevant to their query: +${contextTexts} + +Use this context to inform your answer if it is relevant.`, }); res.setHeader('Content-Type', 'text/event-stream'); @@ -133,4 +162,133 @@ export class AiService { orderBy: { createdAt: 'asc' }, }); } + async analyzeCode(code: string, model = 'llama3') { + const prompt = ` +You are an expert code reviewer. Analyze the following code. +Return ONLY valid JSON with this exact structure: +{ + "summary": "A brief summary of what the code does", + "issues": [ + { "type": "bug", "description": "...", "line": 10 } + ], + "suggestions": ["..."], + "complexity": "O(n)" +} +The 'type' for issues can be 'bug', 'security', 'performance', or 'style'. +If there are no issues, return an empty array for issues. +Do not wrap the JSON in Markdown backticks. Just output raw JSON. + +Code to analyze: +${code} +`; + + try { + const res = await fetch('http://127.0.0.1:11434/api/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model, + prompt, + format: 'json', + stream: false, + }), + }); + + if (!res.ok) throw new Error('Ollama request failed'); + const data = (await res.json()) as { response: string }; + return JSON.parse(data.response) as Record; + } catch (error) { + console.error('Code analysis failed:', error); + throw new Error('Failed to analyze code'); + } + } + + async debugCode(code: string, errorMessage: string, model = 'llama3') { + const prompt = ` +You are an expert software engineer and debugger. +Analyze the following code and the associated error message. +Return ONLY valid JSON with this exact structure: +{ + "rootCause": "Explanation of why the error occurs", + "solution": "High level description of how to fix it", + "fixedCode": "The corrected code snippet" +} +Do not wrap the JSON in Markdown backticks. Just output raw JSON. + +Code: +${code} + +Error Message: +${errorMessage} +`; + + try { + const res = await fetch('http://127.0.0.1:11434/api/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ model, prompt, format: 'json', stream: false }), + }); + if (!res.ok) throw new Error('Ollama request failed'); + const data = (await res.json()) as { response: string }; + return JSON.parse(data.response) as Record; + } catch (error) { + console.error('Code debugging failed:', error); + throw new Error('Failed to debug code'); + } + } + + async generateDocs(code: string, model = 'llama3') { + const prompt = ` +You are a technical writer and senior developer. +Generate comprehensive markdown documentation for the following code. +Include: +- A high-level overview +- Parameters, arguments, and return types (if applicable) +- Examples of how to use it +Output ONLY the markdown documentation. Do not wrap in JSON. + +Code: +${code} +`; + + try { + const res = await fetch('http://127.0.0.1:11434/api/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ model, prompt, stream: false }), + }); + if (!res.ok) throw new Error('Ollama request failed'); + const data = (await res.json()) as { response: string }; + return { documentation: data.response }; + } catch (error) { + console.error('Docs generation failed:', error); + throw new Error('Failed to generate documentation'); + } + } + + async generateEmbeddings(text: string, model = 'nomic-embed-text') { + try { + const res = await fetch('http://127.0.0.1:11434/api/embeddings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ model, prompt: text }), + }); + if (!res.ok) throw new Error('Ollama embeddings request failed'); + const data = (await res.json()) as { embedding: number[] }; + return { embedding: data.embedding }; + } catch (error) { + console.error('Embeddings generation failed:', error); + throw new Error('Failed to generate embeddings'); + } + } + + async getModels() { + try { + const res = await fetch('http://127.0.0.1:11434/api/tags'); + const data = (await res.json()) as { models: Array<{ name: string }> }; + return data.models || []; + } catch { + return [{ name: 'llama3:latest' }]; + } + } } diff --git a/apps/backend/src/notes/dto/create-note.dto.ts b/apps/backend/src/notes/dto/create-note.dto.ts index da4e6b8..91ac9ff 100644 --- a/apps/backend/src/notes/dto/create-note.dto.ts +++ b/apps/backend/src/notes/dto/create-note.dto.ts @@ -10,6 +10,7 @@ export class CreateNoteDto { content?: string; @IsArray() + @IsString({ each: true }) @IsOptional() tags?: string[]; diff --git a/apps/backend/src/notes/dto/update-note.dto.ts b/apps/backend/src/notes/dto/update-note.dto.ts index 4f7c83e..ea01573 100644 --- a/apps/backend/src/notes/dto/update-note.dto.ts +++ b/apps/backend/src/notes/dto/update-note.dto.ts @@ -10,6 +10,7 @@ export class UpdateNoteDto { content?: string; @IsArray() + @IsString({ each: true }) @IsOptional() tags?: string[]; diff --git a/apps/backend/src/notes/notes.module.ts b/apps/backend/src/notes/notes.module.ts index 0184216..4ad9e61 100644 --- a/apps/backend/src/notes/notes.module.ts +++ b/apps/backend/src/notes/notes.module.ts @@ -2,7 +2,10 @@ import { Module } from '@nestjs/common'; import { NotesService } from './notes.service'; import { NotesController } from './notes.controller'; +import { AiModule } from '../ai/ai.module'; + @Module({ + imports: [AiModule], providers: [NotesService], controllers: [NotesController], }) diff --git a/apps/backend/src/notes/notes.service.ts b/apps/backend/src/notes/notes.service.ts index f9ddf19..c2311d3 100644 --- a/apps/backend/src/notes/notes.service.ts +++ b/apps/backend/src/notes/notes.service.ts @@ -8,9 +8,49 @@ import { CreateNoteDto } from './dto/create-note.dto'; import { UpdateNoteDto } from './dto/update-note.dto'; import { Prisma } from '@prisma/client'; +import { AiService } from '../ai/ai.service'; + @Injectable() export class NotesService { - constructor(private prisma: PrismaService) {} + constructor( + private prisma: PrismaService, + private aiService: AiService, + ) {} + + private chunkText(text: string, chunkSize = 500, overlap = 100): string[] { + const chunks: string[] = []; + let i = 0; + while (i < text.length) { + chunks.push(text.substring(i, i + chunkSize)); + i += chunkSize - overlap; + } + return chunks; + } + + private async indexNote(noteId: string, content: string | null) { + if (!content) return; + + // Delete old embeddings + await this.prisma.noteEmbedding.deleteMany({ where: { noteId } }); + + const chunks = this.chunkText(content); + + for (const chunk of chunks) { + if (!chunk.trim()) continue; + + try { + const { embedding } = await this.aiService.generateEmbeddings(chunk); + const embeddingString = `[${embedding.join(',')}]`; + + await this.prisma.$executeRaw` + INSERT INTO "NoteEmbedding" (id, "noteId", content, embedding) + VALUES (gen_random_uuid(), ${noteId}, ${chunk}, ${embeddingString}::vector) + `; + } catch (err) { + console.error('Failed to index chunk for note', noteId, err); + } + } + } async findAll( userId: string, @@ -58,7 +98,7 @@ export class NotesService { } async create(userId: string, dto: CreateNoteDto) { - return this.prisma.note.create({ + const note = await this.prisma.note.create({ data: { ...dto, userId, @@ -72,6 +112,11 @@ export class NotesService { }, }, }); + + // Fire and forget indexing + this.indexNote(note.id, note.content).catch(console.error); + + return note; } async update(id: string, userId: string, dto: UpdateNoteDto) { @@ -81,7 +126,7 @@ export class NotesService { const newContent = dto.content !== undefined ? dto.content : existing.content; - return this.prisma.note.update({ + const updatedNote = await this.prisma.note.update({ where: { id }, data: { ...dto, @@ -95,6 +140,12 @@ export class NotesService { }, }, }); + + if (dto.content !== undefined) { + this.indexNote(updatedNote.id, updatedNote.content).catch(console.error); + } + + return updatedNote; } async remove(id: string, userId: string) { diff --git a/apps/backend/test-script.ts b/apps/backend/test-script.ts new file mode 100644 index 0000000..1a32596 --- /dev/null +++ b/apps/backend/test-script.ts @@ -0,0 +1,10 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +async function main() { + const projects = await prisma.project.findMany({ include: { workspace: true } }); + console.log("Projects:", projects); +} + +main().catch(console.error).finally(() => prisma.$disconnect()); diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json index 0fdd4f7..38b6046 100644 --- a/apps/web/package-lock.json +++ b/apps/web/package-lock.json @@ -8,8 +8,10 @@ "name": "web", "version": "0.1.0", "dependencies": { + "@codemirror/lang-javascript": "^6.2.5", "@hookform/resolvers": "^5.4.0", "@tanstack/react-query": "^5.101.0", + "@uiw/react-codemirror": "^4.25.10", "@uiw/react-md-editor": "^4.1.1", "axios": "^1.17.0", "class-variance-authority": "^0.7.1", @@ -470,6 +472,114 @@ "node": ">=6.9.0" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz", + "integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", + "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz", + "integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz", + "integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz", + "integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.42.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.0.tgz", + "integrity": "sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.43.1", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.1.tgz", + "integrity": "sha512-+BIjw/AG3tDQ4pJgTLPYdAW25eDE66YsvM4LKyVPgGzVgZ4a9Wj1SRX8kPVKgBDdPt8oHtZ15F0qx7p0oOHdHw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.6.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@dotenvx/dotenvx": { "version": "1.71.2", "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.71.2.tgz", @@ -1485,6 +1595,47 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lezer/common": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", + "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", @@ -4142,6 +4293,33 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.25.10", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.10.tgz", + "integrity": "sha512-P3vytLlpE62KYSWrMUnwDCv2lvaQDuDZzyj03mHntuHo5bSl34fRZpjTY3kQTPGuXHxkGSYpoPFFj+hMTqaaMQ==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, "node_modules/@uiw/copy-to-clipboard": { "version": "1.0.21", "resolved": "https://registry.npmjs.org/@uiw/copy-to-clipboard/-/copy-to-clipboard-1.0.21.tgz", @@ -4151,6 +4329,32 @@ "url": "https://jaywcjlove.github.io/#/sponsor" } }, + "node_modules/@uiw/react-codemirror": { + "version": "4.25.10", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.10.tgz", + "integrity": "sha512-DzgSMwM5qzB7v1FIb4gEeriYt67iiay756/HIOM9mAbeOVK0MO7rqefHf0O5c0269pJKMW7AH9FjclExD23V9w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.25.10", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, "node_modules/@uiw/react-markdown-preview": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/@uiw/react-markdown-preview/-/react-markdown-preview-5.2.1.tgz", @@ -5312,6 +5516,21 @@ "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", "license": "MIT" }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -5459,6 +5678,12 @@ } } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -12424,6 +12649,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", @@ -13182,6 +13413,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", diff --git a/apps/web/package.json b/apps/web/package.json index 886bcf5..fb94dbf 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -9,8 +9,10 @@ "lint": "eslint" }, "dependencies": { + "@codemirror/lang-javascript": "^6.2.5", "@hookform/resolvers": "^5.4.0", "@tanstack/react-query": "^5.101.0", + "@uiw/react-codemirror": "^4.25.10", "@uiw/react-md-editor": "^4.1.1", "axios": "^1.17.0", "class-variance-authority": "^0.7.1", diff --git a/apps/web/src/app/dashboard/analysis/page.tsx b/apps/web/src/app/dashboard/analysis/page.tsx new file mode 100644 index 0000000..4676ddd --- /dev/null +++ b/apps/web/src/app/dashboard/analysis/page.tsx @@ -0,0 +1,268 @@ +'use client'; + +import { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import CodeMirror from '@uiw/react-codemirror'; +import { javascript } from '@codemirror/lang-javascript'; +import { + Code, + AlertCircle, + CheckCircle2, + Lightbulb, + Activity, + Loader2, + Play +} from 'lucide-react'; +import api from '@/lib/axios'; +import { cn } from '@/lib/utils'; + +interface AnalysisIssue { + type: 'bug' | 'security' | 'performance' | 'style'; + description: string; + line: number | string; +} + +interface AnalysisResult { + summary: string; + issues: AnalysisIssue[]; + suggestions: string[]; + complexity: string; +} + +export default function CodeAnalysisPage() { + const [code, setCode] = useState('// Paste your code here...\nfunction calculateTotal(items) {\n let total = 0;\n for(let i=0; i<=items.length; i++) {\n total += items[i].price;\n }\n return total;\n}'); + const [selectedModel, setSelectedModel] = useState('llama3'); + const [isAnalyzing, setIsAnalyzing] = useState(false); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + + // Fetch models list + const { data: models = [] } = useQuery({ + queryKey: ['models'], + queryFn: async () => { + const { data } = await api.get<{ name: string }[]>('/ai/models'); + return data; + }, + }); + + const handleAnalyze = async () => { + if (!code.trim()) return; + setIsAnalyzing(true); + setError(null); + setResult(null); + + try { + const { data } = await api.post('/ai/analyze-code', { + code, + model: selectedModel + }); + setResult(data); + } catch (err: any) { + console.error(err); + setError(err.response?.data?.message || 'Failed to analyze code. Make sure Ollama is running and returning valid JSON.'); + } finally { + setIsAnalyzing(false); + } + }; + + const getIssueIcon = (type: string) => { + switch (type.toLowerCase()) { + case 'bug': return ; + case 'security': return ; + case 'performance': return ; + default: return ; + } + }; + + const getIssueColor = (type: string) => { + switch (type.toLowerCase()) { + case 'bug': return 'bg-red-500/10 text-red-500 border-red-500/20'; + case 'security': return 'bg-orange-500/10 text-orange-500 border-orange-500/20'; + case 'performance': return 'bg-blue-500/10 text-blue-500 border-blue-500/20'; + default: return 'bg-yellow-500/10 text-yellow-500 border-yellow-500/20'; + } + }; + + return ( +
+
+
+

+ + AI Code Analysis +

+

+ Paste your code below to get instant feedback, bug detection, and optimization suggestions. +

+
+
+ + + +
+
+ +
+ {/* Editor Panel */} +
+
+ Source Code + JavaScript / TypeScript +
+
+ setCode(val)} + className="text-sm" + basicSetup={{ + lineNumbers: true, + highlightActiveLineGutter: true, + foldGutter: true, + }} + /> +
+
+ + {/* Results Panel */} +
+
+ Analysis Results +
+ +
+ {error && ( +
+ +
{error}
+
+ )} + + {!isAnalyzing && !result && !error && ( +
+ +

Enter code and click analyze to see results

+
+ )} + + {isAnalyzing && ( +
+
+
+ +
+

AI is reviewing your code...

+
+ )} + + {result && !isAnalyzing && ( +
+ {/* Summary Card */} +
+

+ + Summary +

+

+ {result.summary} +

+
+ + {/* Issues */} +
+

+ + Detected Issues + + {result.issues.length} found + +

+ + {result.issues.length === 0 ? ( +
+ + No critical issues detected. Great job! +
+ ) : ( +
+ {result.issues.map((issue, idx) => ( +
+
{getIssueIcon(issue.type)}
+
+
+ {issue.type} + {issue.line && ( + Line: {issue.line} + )} +
+

{issue.description}

+
+
+ ))} +
+ )} +
+ + {/* Suggestions */} + {result.suggestions && result.suggestions.length > 0 && ( +
+

+ + Suggestions +

+
+ {result.suggestions.map((suggestion, idx) => ( +
+
+ {idx + 1} +
+

{suggestion}

+
+ ))} +
+
+ )} + + {/* Complexity */} + {result.complexity && ( +
+
+ + Time Complexity +
+ + {result.complexity} + +
+ )} +
+ )} +
+
+
+
+ ); +} diff --git a/apps/web/src/app/dashboard/chat/page.tsx b/apps/web/src/app/dashboard/chat/page.tsx index 50d1c4f..89db4c0 100644 --- a/apps/web/src/app/dashboard/chat/page.tsx +++ b/apps/web/src/app/dashboard/chat/page.tsx @@ -36,6 +36,7 @@ export default function ChatPage() { const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [isStreaming, setIsStreaming] = useState(false); + const [selectedModel, setSelectedModel] = useState('llama3'); const messagesEndRef = useRef(null); const abortControllerRef = useRef(null); @@ -49,6 +50,15 @@ export default function ChatPage() { }, }); + // Fetch models list + const { data: models = [] } = useQuery({ + queryKey: ['models'], + queryFn: async () => { + const { data } = await api.get<{ name: string }[]>('/ai/models'); + return data; + }, + }); + // Fetch messages when conversationId changes useEffect(() => { if (conversationId) { @@ -84,7 +94,7 @@ export default function ChatPage() { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, - body: JSON.stringify({ message: currentInput, conversationId }), + body: JSON.stringify({ message: currentInput, conversationId, model: selectedModel }), signal: abortControllerRef.current.signal, }); @@ -166,6 +176,21 @@ export default function ChatPage() { New Chat + +
+ + +
{isLoadingConversations ? ( diff --git a/apps/web/src/app/dashboard/debug/page.tsx b/apps/web/src/app/dashboard/debug/page.tsx new file mode 100644 index 0000000..27122da --- /dev/null +++ b/apps/web/src/app/dashboard/debug/page.tsx @@ -0,0 +1,210 @@ +'use client'; + +import { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import CodeMirror from '@uiw/react-codemirror'; +import { javascript } from '@codemirror/lang-javascript'; +import { + Bug, + AlertTriangle, + Lightbulb, + CheckCircle2, + Loader2, + Play +} from 'lucide-react'; +import api from '@/lib/axios'; + +interface DebugResult { + rootCause: string; + solution: string; + fixedCode: string; +} + +export default function DebugPage() { + const [code, setCode] = useState('// Paste broken code here...\nfunction calculateTotal(items) {\n let total = 0;\n for(let i=0; i<=items.length; i++) {\n total += items[i].price;\n }\n return total;\n}'); + const [errorMessage, setErrorMessage] = useState('TypeError: Cannot read properties of undefined (reading \'price\')'); + const [selectedModel, setSelectedModel] = useState('llama3'); + const [isDebugging, setIsDebugging] = useState(false); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + + const { data: models = [] } = useQuery({ + queryKey: ['models'], + queryFn: async () => { + const { data } = await api.get<{ name: string }[]>('/ai/models'); + return data; + }, + }); + + const handleDebug = async () => { + if (!code.trim() || !errorMessage.trim()) return; + setIsDebugging(true); + setError(null); + setResult(null); + + try { + const { data } = await api.post('/ai/debug', { + code, + errorMessage, + model: selectedModel + }); + setResult(data); + } catch (err: any) { + console.error(err); + setError(err.response?.data?.message || 'Failed to debug code. Ensure Ollama is running and returning valid JSON.'); + } finally { + setIsDebugging(false); + } + }; + + return ( +
+
+
+

+ + AI Debugger +

+

+ Paste your broken code and the error stack trace to get an instant root cause and fix. +

+
+
+ + + +
+
+ +
+ {/* Input Panel */} +
+
+
+ + Source Code +
+
+ setCode(val)} + className="text-sm h-full" + basicSetup={{ lineNumbers: true }} + /> +
+
+ +
+
+ + Error Message / Stack Trace +
+