diff --git a/app/(auth)/auth.ts b/app/(auth)/auth.ts index 99ffd0c..e18d647 100644 --- a/app/(auth)/auth.ts +++ b/app/(auth)/auth.ts @@ -1,13 +1,12 @@ -import { compare } from 'bcrypt-ts'; -import NextAuth, { type User, type Session } from 'next-auth'; -import Credentials from 'next-auth/providers/credentials'; +import { compare } from "bcrypt-ts"; +import NextAuth, { type User, type Session } from "next-auth"; +import Credentials from "next-auth/providers/credentials"; import GoogleProvider from "next-auth/providers/google"; -import { DrizzleAdapter } from "@auth/drizzle-adapter" +import { DrizzleAdapter } from "@auth/drizzle-adapter"; -import { db, getUser } from '@/lib/db/queries'; - -import { authConfig } from './auth.config'; -import { accounts, user } from '@/lib/db/schema'; +import { db, getUser } from "@/lib/db/queries"; +import { authConfig } from "./auth.config"; +import { accounts, user } from "@/lib/db/schema"; interface ExtendedSession extends Session { user: User; @@ -26,29 +25,26 @@ export const { accountsTable: accounts, }), providers: [ + GoogleProvider({ + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }), // Credentials({ // credentials: {}, // async authorize({ email, password }: any) { // const users = await getUser(email); // if (users.length === 0) return null; - // // biome-ignore lint: Forbidden non-null assertion. // const passwordsMatch = await compare(password, users[0].password!); // if (!passwordsMatch) return null; // return users[0] as any; // }, // }), - GoogleProvider({ - allowDangerousEmailAccountLinking: true, - clientId: process.env.GOOGLE_CLIENT_ID, - clientSecret: process.env.GOOGLE_CLIENT_SECRET, - }) ], callbacks: { async jwt({ token, user }) { if (user) { token.id = user.id; } - return token; }, async session({ @@ -61,7 +57,6 @@ export const { if (session.user) { session.user.id = token.id as string; } - return session; }, }, diff --git a/app/(auth)/authbutton.tsx b/app/(auth)/authbutton.tsx new file mode 100644 index 0000000..31cc335 --- /dev/null +++ b/app/(auth)/authbutton.tsx @@ -0,0 +1,36 @@ +"use client" + +import { signIn, signOut, useSession } from "next-auth/react" + +export default function AuthButton() { + const { data: session, status } = useSession() + + if (status === "loading") { + return
Loading...
+ } + + if (session?.user) { + return ( +
+ + {session.user.email} + + +
+ ) + } + + return ( + + ) +} diff --git a/app/favicon.ico b/app/favicon.ico index b854b21..5d545a0 100644 Binary files a/app/favicon.ico and b/app/favicon.ico differ diff --git a/app/layout.tsx b/app/layout.tsx index a4710e0..75ca874 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -8,7 +8,40 @@ import DatadogInit from '@/components/datadog-init'; import './globals.css'; -export const metadata: Metadata = BASE_METADATA; +export const metadata: Metadata = { + ...BASE_METADATA, + title: "Virgo Chat", + description: "Chat directly with 2,800+ APIs in one place, powered by Virgo.", + icons: { + icon: "/images/profileimg.jpg", + shortcut: "/images/profileimg.jpg", + apple: "/images/profileimg.jpg", + }, + openGraph: { + title: "Virgo Chat", + description: "Chat directly with 2,800+ APIs in one place, powered by Virgo.", + url: "https://virgo-mcp-chat.vercel.app", + siteName: "Virgo MCP", + images: [ + { + url: "/images/og-image.png", // <-- place a 1200x630 image in /public/images + width: 1200, + height: 630, + alt: "Virgo MCP Chat Preview", + }, + ], + locale: "en_US", + type: "website", + }, + twitter: { + card: "summary_large_image", + title: "Virgo Chat", + description: "Chat directly with 2,800+ APIs in one place, powered by Virgo.", + images: ["/images/og-image.png"], + }, +}; + + export const viewport = { maximumScale: 1, // Disable auto-zoom on mobile Safari diff --git a/components/chat-header.tsx b/components/chat-header.tsx index ac9f219..4c143ac 100644 --- a/components/chat-header.tsx +++ b/components/chat-header.tsx @@ -1,13 +1,9 @@ "use client" import { useRouter } from "next/navigation" -import { useEffectiveSession } from '@/hooks/use-effective-session' - import { ModelSelector } from "@/components/model-selector" import { SidebarToggle } from "@/components/sidebar-toggle" import { Button } from "@/components/ui/button" -import { GitHubButton } from "@/components/github-button" -import { DocsButton } from "@/components/docs-button" import { memo } from "react" import { PlusIcon } from "./icons" import { useSidebar } from "./ui/sidebar" @@ -27,117 +23,54 @@ function PureChatHeader({ }) { const router = useRouter() const { open } = useSidebar() - const { data: session } = useEffectiveSession() - const isSignedIn = !!session?.user - - // Don't render the header for signed-out users - if (!isSignedIn) { - return null - } return ( -
- {/* Always show sidebar toggle */} -
- -
- - {/* Mobile layout: Show controls in left-to-right order with new chat button on the right */} - {!isReadonly && ( -
- -
- )} - - {!isReadonly && ( -
- -
- )} - - {/* Spacer to push new chat button to the right on mobile */} -
- - {/* Mobile new chat button - positioned on the right with full text */} -
- - - - - New Chat - +
+ {/* Virgo Logo in top-left */} +
+ Virgo Logo
- {/* Desktop new chat button - hidden when sidebar is open to avoid overlap */} -
- - - - - New Chat - + {/* Sidebar toggle */} +
+
- {/* Desktop layout: Show controls after new chat button */} + {/* Model + Visibility selectors */} {!isReadonly && ( -
- +
+ +
)} - {!isReadonly && ( -
- -
- )} + {/* Spacer pushes new chat button to the right */} +
- {/* Spacer to push buttons to the right on desktop */} -
- -
- - -
+ {/* New chat button */} + + + + + New Chat +
) } -export const ChatHeader = memo(PureChatHeader, (prevProps, nextProps) => { - return prevProps.selectedModelId === nextProps.selectedModelId -}) +export const ChatHeader = memo( + PureChatHeader, + (prevProps, nextProps) => { + return prevProps.selectedModelId === nextProps.selectedModelId + } +) diff --git a/components/chat.tsx b/components/chat.tsx index 7014d1e..5722462 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -117,24 +117,24 @@ export function Chat({ {messages.length === 0 ? (
-
-

Welcome to MCP Chat by Pipedream

- - Alpha - -
-

- Chat directly with 2,800+ APIs powered by {" "} - - Pipedream Connect - -

+
+

+ Welcome to Virgo Chat +

+ Profile + + Alpha + +
+ +

+ Chat directly with 2,800+ APIs +

+
{/* Centered input form for home page */} diff --git a/components/info-banner.tsx b/components/info-banner.tsx index 04421a0..1bee281 100644 --- a/components/info-banner.tsx +++ b/components/info-banner.tsx @@ -12,9 +12,6 @@ interface InfoBannerProps { /** * Custom hook for managing dismissible banner states. - * - * This demonstrates a pattern for handling multiple dismissible UI elements - * with a single, reusable state management solution. */ function useDismissibleBanners() { const [dismissedBanners, setDismissedBanners] = useState>(new Set()); @@ -32,9 +29,6 @@ function useDismissibleBanners() { /** * Reusable banner component for displaying dismissible messages. - * - * This component demonstrates how to create themed, dismissible banners - * with consistent styling. */ function Banner({ theme, @@ -73,15 +67,7 @@ function Banner({ } /** - * InfoBanner displays contextual information about the app's current configuration. - * - * Shows different banners based on: - * - Auth disabled: Warning about development mode - * - Persistence disabled: Warning about chat storage - * - Normal mode: Info about the MCP demo - * - * This pattern allows developers to understand the current app state and - * provides helpful links to documentation. + * InfoBanner for Virgo branding (replaces Pipedream copy). */ export function InfoBanner({ isAuthDisabled: isAuthDisabledMode, isPersistenceDisabled = false, className = "" }: InfoBannerProps) { const { dismissBanner, isBannerDismissed } = useDismissibleBanners(); @@ -89,7 +75,6 @@ export function InfoBanner({ isAuthDisabled: isAuthDisabledMode, isPersistenceDi const showMainBanner = !isBannerDismissed('main'); const showPersistenceWarning = isPersistenceDisabled && !isBannerDismissed('persistence'); - // Don't render anything if both banners are dismissed if (!showMainBanner && !showPersistenceWarning) { return null; } @@ -102,19 +87,12 @@ export function InfoBanner({ isAuthDisabled: isAuthDisabledMode, isPersistenceDi onDismiss={() => dismissBanner('main')} > {isAuthDisabledMode ? ( - User sign-in is currently disabled, make sure to enable before shipping to production. + + User sign-in is currently disabled. You can still try Virgo Chat in prototype mode. + ) : ( - This demo app showcases how you can integrate Pipedream's MCP server into your AI app.{' '} - - Check out our docs - - {' '} to get started. + Welcome to Virgo Chat — chat directly with 2,800+ APIs in one place. )} @@ -130,4 +108,4 @@ export function InfoBanner({ isAuthDisabled: isAuthDisabledMode, isPersistenceDi )}
); -} \ No newline at end of file +} diff --git a/components/overview.tsx b/components/overview.tsx index 3eeca35..8d74cfa 100644 --- a/components/overview.tsx +++ b/components/overview.tsx @@ -1,5 +1,4 @@ import { motion } from 'framer-motion'; -import Link from 'next/link'; import Image from 'next/image'; import { ChatBubbleIcon } from './icons'; @@ -20,43 +19,40 @@ export const Overview = () => { >
-

- Pipedream - + - -

+ {/* Virgo logo + chat icon */} +

+ Virgo Logo + + + +

+ + + {/* Custom Virgo copy */}

- This app uses{" "} - - Pipedream MCP - {" "} - to let you chat with any app. + Welcome to Virgo Chat — + your gateway to connect and chat with any app.

- With {" "} - - 2,800+ built-in APIs - {" "} - {" "}and 10k+ tools, use Pipedream MCP to supercharge your AI app or agent. + Chat directly with 2,800+ APIs + and 10k+ tools. Powered by Virgo, + built for speed and simplicity.

- + + {/* Keep the banner logic */} +
); }; + diff --git a/components/signed-out-header.tsx b/components/signed-out-header.tsx index a9b81c0..387b77f 100644 --- a/components/signed-out-header.tsx +++ b/components/signed-out-header.tsx @@ -2,46 +2,19 @@ import Image from 'next/image'; import Link from 'next/link'; -import { useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { GitHubButton } from '@/components/github-button'; -import { SignInModal } from './sign-in-modal'; -import { useAuthContext } from './session-provider'; -import { DocsButton } from './docs-button'; export function SignedOutHeader() { - const [isSignInModalOpen, setIsSignInModalOpen] = useState(false); - const { isAuthDisabled } = useAuthContext(); - - const handleGetStarted = () => { - setIsSignInModalOpen(true); - }; - return (
Pipedream - -
- - - -
- - setIsSignInModalOpen(false)} - />
); -} \ No newline at end of file +} diff --git a/drizzle.config.ts b/drizzle.config.ts index 7c4a841..f3c2a07 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -10,7 +10,6 @@ export default defineConfig({ out: './lib/db/migrations', dialect: 'postgresql', dbCredentials: { - // biome-ignore lint: Forbidden non-null assertion. - url: process.env.POSTGRES_URL!, + url: process.env.DATABASE_URL!, // switched from POSTGRES_URL }, }); diff --git a/lib/db/index.ts b/lib/db/index.ts new file mode 100644 index 0000000..ccd3c58 --- /dev/null +++ b/lib/db/index.ts @@ -0,0 +1,13 @@ +import { drizzle } from "drizzle-orm/neon-http"; +import { neon } from "@neondatabase/serverless"; + +// Ensure DATABASE_URL is set +if (!process.env.DATABASE_URL) { + throw new Error("DATABASE_URL is not set in environment variables"); +} + +// Neon client +const sql = neon(process.env.DATABASE_URL!); + +// Export a Drizzle instance +export const db = drizzle(sql); diff --git a/lib/db/schema.ts b/lib/db/schema.ts index 53340d7..1509f54 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -4,7 +4,6 @@ import { varchar, timestamp, json, - uuid, text, primaryKey, foreignKey, @@ -13,19 +12,25 @@ import { } from "drizzle-orm/pg-core" import type { AdapterAccountType } from "next-auth/adapters" +// +// Users +// export const user = pgTable("User", { - id: uuid("id").primaryKey().notNull().defaultRandom(), + id: text("id").primaryKey().notNull(), // was uuid email: varchar("email", { length: 64 }).unique(), password: varchar("password", { length: 64 }), }) export type User = InferSelectModel +// +// Chats +// export const chat = pgTable("Chat", { - id: uuid("id").primaryKey().notNull().defaultRandom(), + id: text("id").primaryKey().notNull(), createdAt: timestamp("createdAt").notNull(), title: text("title").notNull(), - userId: uuid("userId") + userId: text("userId") .notNull() .references(() => user.id), visibility: varchar("visibility", { enum: ["public", "private"] }) @@ -35,10 +40,13 @@ export const chat = pgTable("Chat", { export type Chat = InferSelectModel +// +// Accounts +// export const accounts = pgTable( "account", { - userId: uuid("userId") + userId: text("userId") .notNull() .references(() => user.id, { onDelete: "cascade" }), type: text("type").$type().notNull(), @@ -61,11 +69,12 @@ export const accounts = pgTable( ] ) -// DEPRECATED: The following schema is deprecated and will be removed in the future. -// Read the migration guide at https://github.com/vercel/ai-chatbot/blob/main/docs/04-migrate-to-parts.md +// +// Deprecated Messages +// export const messageDeprecated = pgTable("Message", { - id: uuid("id").primaryKey().notNull().defaultRandom(), - chatId: uuid("chatId") + id: text("id").primaryKey().notNull(), + chatId: text("chatId") .notNull() .references(() => chat.id), role: varchar("role").notNull(), @@ -75,9 +84,12 @@ export const messageDeprecated = pgTable("Message", { export type MessageDeprecated = InferSelectModel +// +// Messages v2 +// export const message = pgTable("Message_v2", { - id: uuid("id").primaryKey().notNull().defaultRandom(), - chatId: uuid("chatId") + id: text("id").primaryKey().notNull(), + chatId: text("chatId") .notNull() .references(() => chat.id), role: varchar("role").notNull(), @@ -88,15 +100,16 @@ export const message = pgTable("Message_v2", { export type DBMessage = InferSelectModel -// DEPRECATED: The following schema is deprecated and will be removed in the future. -// Read the migration guide at https://github.com/vercel/ai-chatbot/blob/main/docs/04-migrate-to-parts.md +// +// Deprecated Votes +// export const voteDeprecated = pgTable( "Vote", { - chatId: uuid("chatId") + chatId: text("chatId") .notNull() .references(() => chat.id), - messageId: uuid("messageId") + messageId: text("messageId") .notNull() .references(() => messageDeprecated.id), isUpvoted: boolean("isUpvoted").notNull(), @@ -110,13 +123,16 @@ export const voteDeprecated = pgTable( export type VoteDeprecated = InferSelectModel +// +// Votes v2 +// export const vote = pgTable( "Vote_v2", { - chatId: uuid("chatId") + chatId: text("chatId") .notNull() .references(() => chat.id), - messageId: uuid("messageId") + messageId: text("messageId") .notNull() .references(() => message.id), isUpvoted: boolean("isUpvoted").notNull(), @@ -130,17 +146,20 @@ export const vote = pgTable( export type Vote = InferSelectModel +// +// Documents +// export const document = pgTable( "Document", { - id: uuid("id").notNull().defaultRandom(), + id: text("id").notNull(), createdAt: timestamp("createdAt").notNull(), title: text("title").notNull(), content: text("content"), kind: varchar("text", { enum: ["text", "code", "image", "sheet"] }) .notNull() .default("text"), - userId: uuid("userId") + userId: text("userId") .notNull() .references(() => user.id), }, @@ -153,17 +172,20 @@ export const document = pgTable( export type Document = InferSelectModel +// +// Suggestions +// export const suggestion = pgTable( "Suggestion", { - id: uuid("id").notNull().defaultRandom(), - documentId: uuid("documentId").notNull(), + id: text("id").notNull(), + documentId: text("documentId").notNull(), documentCreatedAt: timestamp("documentCreatedAt").notNull(), originalText: text("originalText").notNull(), suggestedText: text("suggestedText").notNull(), description: text("description"), isResolved: boolean("isResolved").notNull().default(false), - userId: uuid("userId") + userId: text("userId") .notNull() .references(() => user.id), createdAt: timestamp("createdAt").notNull(), diff --git a/lib/pd-backend-client.ts b/lib/pd-backend-client.ts index ccf7c6b..36f0bbc 100644 --- a/lib/pd-backend-client.ts +++ b/lib/pd-backend-client.ts @@ -8,13 +8,14 @@ export function pdClient(): PipedreamClient { return _pd; } -export const pdHeaders = async (exuid: string) => { +export const pdHeaders = async (exuid?: string) => { const accessToken = await pdClient().rawAccessToken; return { Authorization: `Bearer ${accessToken}`, "x-pd-project-id": process.env.PIPEDREAM_PROJECT_ID, "x-pd-environment": process.env.PIPEDREAM_PROJECT_ENVIRONMENT, - "x-pd-external-user-id": exuid, + // Use provided userId OR fallback to "virgo" + "x-pd-external-user-id": exuid || "virgo", }; }; diff --git a/mods/mcp-client.ts b/mods/mcp-client.ts index f24da3a..8ae128a 100644 --- a/mods/mcp-client.ts +++ b/mods/mcp-client.ts @@ -125,14 +125,21 @@ class MCPSessionManager { private chatId: string private userId: string - constructor(mcpBaseUrl: string, userId: string, chatId: string, sessionId: string | undefined) { - console.log(`Using ${mcpBaseUrl} as the MCP Server.`) - this.serverUrl = `${mcpBaseUrl}/v1/${userId}` - this.sessionId = sessionId - this.chatId = chatId - this.userId = userId - console.log(`Creating MCP Session: ${this.serverUrl} chatId=${this.chatId} sessionId=${this.sessionId}`) - } + constructor(mcpBaseUrl: string, userId: string | undefined, chatId: string, sessionId: string | undefined) { + console.log(`Using ${mcpBaseUrl} as the MCP Server.`) + + // Default to "virgo" if no user ID is provided (prototype mode) + this.userId = userId || "virgo" + + this.serverUrl = `${mcpBaseUrl}/v1/${this.userId}` + this.sessionId = sessionId + this.chatId = chatId + + console.log( + `Creating MCP Session: ${this.serverUrl} chatId=${this.chatId} sessionId=${this.sessionId}` + ) +} + /** * Connects to the MCP SSE endpoint and initializes the session diff --git a/package.json b/package.json index ef3478c..c7d5bbe 100644 --- a/package.json +++ b/package.json @@ -2,21 +2,23 @@ "name": "mcp-chat", "version": "0.1.0", "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint && biome lint --write --unsafe", - "lint:fix": "next lint --fix && biome lint --write --unsafe", - "format": "biome format --write", - "db:generate": "drizzle-kit generate", - "db:migrate": "npx tsx lib/db/migrate.ts", - "db:studio": "drizzle-kit studio", - "db:push": "drizzle-kit push", - "db:pull": "drizzle-kit pull", - "db:check": "drizzle-kit check", - "db:up": "drizzle-kit up" - }, +"scripts": { + "dev": "next dev", + "build": "drizzle-kit push && next build", + "start": "next start", + "lint": "next lint && biome lint --write --unsafe", + "lint:fix": "next lint --fix && biome lint --write --unsafe", + "format": "biome format --write", + "db:generate": "drizzle-kit generate", + "db:migrate": "npx tsx lib/db/migrate.ts", + "db:studio": "drizzle-kit studio", + "db:push": "drizzle-kit push", + "db:pull": "drizzle-kit pull", + "db:check": "drizzle-kit check", + "db:up": "drizzle-kit up" +}, + + "dependencies": { "@ai-sdk/anthropic": "^1.2.0", "@ai-sdk/fireworks": "^0.1.16", diff --git a/public/images/Logo-01.png b/public/images/Logo-01.png new file mode 100644 index 0000000..6f5d3f6 Binary files /dev/null and b/public/images/Logo-01.png differ diff --git a/public/images/profileimg.jpg b/public/images/profileimg.jpg new file mode 100644 index 0000000..5d545a0 Binary files /dev/null and b/public/images/profileimg.jpg differ