From 7824005a9a7e8f7ac6e3f88f432efd4463c82573 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 15:26:01 +0000 Subject: [PATCH 01/15] Initial plan From 252b214613cc693cffa31009a9d3c89924716b1e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 15:34:47 +0000 Subject: [PATCH 02/15] feat: Add GitHub Models AI Assistant integration - Install Azure packages for GitHub Models API - Add COPILOT_GITHUB_TOKEN environment variable - Create API route at /api/ai/chat with validation - Build AI chat interface with shadcn/ui components - Add AI Assistant to dashboard navigation - Type check and lint passed, build successful Co-authored-by: syed-reza98 <71028588+syed-reza98@users.noreply.github.com> --- .env.example | 3 + package-lock.json | 163 +++++++++ package.json | 3 + src/app/api/ai/chat/route.ts | 165 +++++++++ .../ai-assistant/ai-assistant-client.tsx | 339 ++++++++++++++++++ src/app/dashboard/ai-assistant/page.tsx | 39 ++ src/components/app-sidebar.tsx | 6 + src/lib/env.ts | 4 + 8 files changed, 722 insertions(+) create mode 100644 src/app/api/ai/chat/route.ts create mode 100644 src/app/dashboard/ai-assistant/ai-assistant-client.tsx create mode 100644 src/app/dashboard/ai-assistant/page.tsx diff --git a/.env.example b/.env.example index 10f313b0..8686f41b 100644 --- a/.env.example +++ b/.env.example @@ -16,3 +16,6 @@ NEXTAUTH_URL="http://localhost:3000" # Email Configuration EMAIL_FROM="noreply@example.com" RESEND_API_KEY="re_dummy_key_for_build" # Build fails without this + +# GitHub Models AI Configuration +COPILOT_GITHUB_TOKEN="" # GitHub PAT for accessing GitHub Models AI diff --git a/package-lock.json b/package-lock.json index 5794fd7b..456c4e76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,9 @@ "hasInstallScript": true, "dependencies": { "@auth/prisma-adapter": "^2.11.1", + "@azure-rest/ai-inference": "^1.0.0-beta.6", + "@azure/core-auth": "^1.10.1", + "@azure/core-sse": "^2.3.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", @@ -200,6 +203,152 @@ "@prisma/client": ">=2.26.0 || >=3 || >=4 || >=5 || >=6" } }, + "node_modules/@azure-rest/ai-inference": { + "version": "1.0.0-beta.6", + "resolved": "https://registry.npmjs.org/@azure-rest/ai-inference/-/ai-inference-1.0.0-beta.6.tgz", + "integrity": "sha512-j5FrJDTHu2P2+zwFVe5j2edasOIhqkFj+VkDjbhGkQuOoIAByF0egRkgs0G1k03HyJ7bOOT9BkRF7MIgr/afhw==", + "license": "MIT", + "dependencies": { + "@azure-rest/core-client": "^2.1.0", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-lro": "^2.7.2", + "@azure/core-rest-pipeline": "^1.18.2", + "@azure/core-tracing": "^1.2.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure-rest/core-client": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.5.1.tgz", + "integrity": "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz", + "integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-sse": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@azure/core-sse/-/core-sse-2.3.0.tgz", + "integrity": "sha512-jKhPpdDbVS5GlpadSKIC7V6Q4P2vEcwXi1c4CLTXs01Q/PAITES9v5J/S73+RtCMqQpsX0jGa2yPWwXi9JzdgA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -4612,6 +4761,20 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.2.tgz", + "integrity": "sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg==", + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@unrs/resolver-binding-android-arm-eabi": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", diff --git a/package.json b/package.json index eda63e6a..dd352dd0 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,9 @@ }, "dependencies": { "@auth/prisma-adapter": "^2.11.1", + "@azure-rest/ai-inference": "^1.0.0-beta.6", + "@azure/core-auth": "^1.10.1", + "@azure/core-sse": "^2.3.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", diff --git a/src/app/api/ai/chat/route.ts b/src/app/api/ai/chat/route.ts new file mode 100644 index 00000000..a22016b6 --- /dev/null +++ b/src/app/api/ai/chat/route.ts @@ -0,0 +1,165 @@ +// src/app/api/ai/chat/route.ts +// GitHub Models AI Chat Endpoint + +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/lib/auth'; +import { z } from 'zod'; +import ModelClient, { isUnexpected } from "@azure-rest/ai-inference"; +import { AzureKeyCredential } from "@azure/core-auth"; + +// Request validation schema +const chatRequestSchema = z.object({ + message: z.string().min(1, "Message is required").max(4000, "Message too long"), + model: z.string().optional().default("meta-llama/Llama-3.2-11B-Vision-Instruct"), + temperature: z.number().min(0).max(2).optional().default(1.0), + maxTokens: z.number().min(1).max(4000).optional().default(1000), + conversationHistory: z.array(z.object({ + role: z.enum(["user", "assistant", "system"]), + content: z.string() + })).optional().default([]), +}); + +export type ChatRequest = z.infer; + +// POST /api/ai/chat - Send message to GitHub Models AI +export async function POST(request: NextRequest) { + try { + // Check authentication + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + // Check if GitHub token is configured + const token = process.env.COPILOT_GITHUB_TOKEN; + if (!token) { + return NextResponse.json( + { + error: 'GitHub Models is not configured. Please add COPILOT_GITHUB_TOKEN to environment variables.', + configured: false + }, + { status: 503 } + ); + } + + // Parse and validate request body + const body = await request.json(); + const validationResult = chatRequestSchema.safeParse(body); + + if (!validationResult.success) { + return NextResponse.json( + { + error: 'Invalid request', + details: validationResult.error.issues + }, + { status: 400 } + ); + } + + const { message, model, temperature, maxTokens, conversationHistory } = validationResult.data; + + // Initialize GitHub Models client + const client = ModelClient( + "https://models.github.ai/inference", + new AzureKeyCredential(token) + ); + + // Build messages array with conversation history + const messages = [ + ...conversationHistory, + { role: "user" as const, content: message } + ]; + + // Call GitHub Models API + const response = await client.path("/chat/completions").post({ + body: { + messages, + model, + temperature, + max_tokens: maxTokens, + top_p: 1.0 + } + }); + + // Check for errors + if (isUnexpected(response)) { + console.error('GitHub Models API error:', response.body); + return NextResponse.json( + { + error: 'AI service error', + details: response.body.error?.message || 'Unknown error' + }, + { status: 500 } + ); + } + + // Extract response content + const assistantMessage = response.body.choices[0]?.message?.content; + + if (!assistantMessage) { + return NextResponse.json( + { error: 'No response from AI model' }, + { status: 500 } + ); + } + + // Return successful response + return NextResponse.json({ + message: assistantMessage, + model: response.body.model, + usage: response.body.usage, + finishReason: response.body.choices[0]?.finish_reason, + }); + + } catch (error) { + console.error('POST /api/ai/chat error:', error); + + // Handle specific error types + if (error instanceof SyntaxError) { + return NextResponse.json( + { error: 'Invalid JSON in request body' }, + { status: 400 } + ); + } + + return NextResponse.json( + { + error: 'Failed to process AI request', + details: error instanceof Error ? error.message : 'Unknown error' + }, + { status: 500 } + ); + } +} + +// GET /api/ai/chat - Check AI service status +export async function GET() { + try { + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + const token = process.env.COPILOT_GITHUB_TOKEN; + + return NextResponse.json({ + configured: !!token, + available: !!token, + endpoint: "https://models.github.ai/inference", + defaultModel: "meta-llama/Llama-3.2-11B-Vision-Instruct", + }); + } catch (error) { + console.error('GET /api/ai/chat error:', error); + return NextResponse.json( + { error: 'Failed to check AI status' }, + { status: 500 } + ); + } +} diff --git a/src/app/dashboard/ai-assistant/ai-assistant-client.tsx b/src/app/dashboard/ai-assistant/ai-assistant-client.tsx new file mode 100644 index 00000000..09e59d4c --- /dev/null +++ b/src/app/dashboard/ai-assistant/ai-assistant-client.tsx @@ -0,0 +1,339 @@ +"use client"; + +import * as React from "react"; +import { useState, useRef, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Textarea } from "@/components/ui/textarea"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + IconSend, + IconRobot, + IconUser, + IconAlertCircle, + IconLoader2, + IconSparkles, + IconTrash, +} from "@tabler/icons-react"; +import { toast } from "sonner"; + +interface Message { + id: string; + role: "user" | "assistant"; + content: string; + timestamp: Date; +} + +interface AIStatus { + configured: boolean; + available: boolean; + endpoint: string; + defaultModel: string; +} + +export function AIAssistantClient() { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [status, setStatus] = useState(null); + const [isCheckingStatus, setIsCheckingStatus] = useState(true); + const scrollRef = useRef(null); + + // Check AI service status on mount + useEffect(() => { + checkStatus(); + }, []); + + // Auto-scroll to bottom when new messages arrive + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollIntoView({ behavior: "smooth" }); + } + }, [messages]); + + const checkStatus = async () => { + try { + setIsCheckingStatus(true); + const response = await fetch("/api/ai/chat"); + if (response.ok) { + const data = await response.json(); + setStatus(data); + } + } catch (error) { + console.error("Failed to check AI status:", error); + toast.error("Failed to check AI service status"); + } finally { + setIsCheckingStatus(false); + } + }; + + const sendMessage = async () => { + if (!input.trim() || isLoading) return; + + const userMessage: Message = { + id: Date.now().toString(), + role: "user", + content: input.trim(), + timestamp: new Date(), + }; + + setMessages((prev) => [...prev, userMessage]); + setInput(""); + setIsLoading(true); + + try { + // Build conversation history for context + const conversationHistory = messages.map(msg => ({ + role: msg.role, + content: msg.content, + })); + + const response = await fetch("/api/ai/chat", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + message: userMessage.content, + conversationHistory, + }), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || "Failed to get AI response"); + } + + const assistantMessage: Message = { + id: (Date.now() + 1).toString(), + role: "assistant", + content: data.message, + timestamp: new Date(), + }; + + setMessages((prev) => [...prev, assistantMessage]); + + // Show usage info if available + if (data.usage) { + console.log("Token usage:", data.usage); + } + } catch (error) { + console.error("Error sending message:", error); + toast.error(error instanceof Error ? error.message : "Failed to send message"); + + // Remove the user message if there was an error + setMessages((prev) => prev.filter((msg) => msg.id !== userMessage.id)); + } finally { + setIsLoading(false); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + sendMessage(); + } + }; + + const clearConversation = () => { + setMessages([]); + toast.success("Conversation cleared"); + }; + + if (isCheckingStatus) { + return ( +
+ + +
+ ); + } + + if (!status?.configured) { + return ( +
+ + + + + AI Assistant Not Configured + + + GitHub Models integration requires configuration + + + + + + +
+

To use the AI Assistant, you need to:

+
    +
  1. + Generate a GitHub Personal Access Token (PAT) with access to GitHub Models +
  2. +
  3. + Add the token to your environment variables as COPILOT_GITHUB_TOKEN +
  4. +
  5. Restart the application
  6. +
+

+ Learn more:{" "} + + GitHub PAT Documentation + +

+
+
+
+
+
+
+ ); + } + + return ( +
+
+
+

+ + AI Assistant +

+

+ Powered by GitHub Models - {status?.defaultModel} +

+
+ {messages.length > 0 && ( + + )} +
+ + + + Conversation + + Ask me anything about machine learning, coding, or general knowledge + + + + + {messages.length === 0 ? ( +
+ +

Start a Conversation

+

+ Type your message below to get started. I can help you with coding questions, + explain concepts, or have a general conversation. +

+
+ ) : ( +
+ {messages.map((message) => ( +
+ {message.role === "assistant" && ( +
+
+ +
+
+ )} +
+
+

{message.content}

+
+ + {message.timestamp.toLocaleTimeString()} + +
+ {message.role === "user" && ( +
+
+ +
+
+ )} +
+ ))} + {isLoading && ( +
+
+
+ +
+
+
+ + Thinking... +
+
+ )} +
+
+ )} + + +
+
+