From cc38af6e4040194d0435f4bbd94d1d6349204b21 Mon Sep 17 00:00:00 2001 From: ismael garcia Date: Thu, 26 Feb 2026 11:55:04 +0000 Subject: [PATCH] GITHUB COPILOT TEST --- .github/copilot-instructions.md | 46 ++ .../connect/app/pages/app/business/[id].vue | 470 ++++++++++++++++++ .../app/pages/app/business/initial.vue | 2 +- .../server/api/v1/business/[id].get.ts | 34 ++ .../business/engagement-targets/[id].post.ts | 70 +++ .../api/v1/ai/business/generate-blog.post.ts | 49 ++ .../api/v1/ai/business/geo-audit/[id].post.ts | 68 +++ .../api/v1/ai/business/grow-plan/[id].post.ts | 66 +++ .../api/v1/ai/business/seo-audit/[id].get.ts | 55 ++ .../server/api/v1/ai/generate.post.ts | 86 ++-- packages/scheduler/server/utils/ai.ts | 92 ++++ 11 files changed, 992 insertions(+), 46 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 packages/connect/app/pages/app/business/[id].vue create mode 100644 packages/connect/server/api/v1/business/[id].get.ts create mode 100644 packages/scheduler/server/api/v1/ai/business/engagement-targets/[id].post.ts create mode 100644 packages/scheduler/server/api/v1/ai/business/generate-blog.post.ts create mode 100644 packages/scheduler/server/api/v1/ai/business/geo-audit/[id].post.ts create mode 100644 packages/scheduler/server/api/v1/ai/business/grow-plan/[id].post.ts create mode 100644 packages/scheduler/server/api/v1/ai/business/seo-audit/[id].get.ts create mode 100644 packages/scheduler/server/utils/ai.ts diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..ecfa2e4d --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,46 @@ + +# Copilot instructions — MagicSync (monorepo) + +Goal: help an AI coding agent become productive quickly in this pnpm/Nuxt monorepo. + +- Big picture: This is a Nuxt-based monorepo with workspace packages under `packages/*`. Key layers are: `db`, `auth`, `assets`, `scheduler`, `connect`, `tools`, `ai-tools`, `bulk-scheduler`, `content`, `site`, `ui`, `email`, `doc`. The root orchestrates per-layer commands via `pnpm` scripts in `package.json`. + +- How to run and common workflows: + - Use the repo `pnpm` commands. The root `package.json` exposes shortcuts like `pnpm packages ui dev`, `pnpm packages site dev`, `pnpm packages db db:migrate`. + - Install: `pnpm i` (root). Project uses `pnpm@10.x` and workspaces configured at `packages/*`. + - DB setup (from README): `pnpm packages db db:generate` then `pnpm packages db db:migrate`. + - Per-layer dev: `pnpm packages [layer] dev` (example: `pnpm packages ui dev`). Production start: `pnpm packages [layer] start`. + - Docker: repository includes `docker-compose.yml` and `docker-compose.dev.yml` for containerized runs; prefer local `pnpm` devs for iterative dev unless reproducing production. + +- Environment and important globals: + - Check `config/env.config.ts` for required env vars. Notable required keys: `NUXT_TURSO_DATABASE_URL`, `NUXT_TURSO_AUTH_TOKEN`, `NUXT_PEXELS_API_KEY`. + - There is an expected `.env` (use `.env-example` if present). Many layer dev scripts rely on `dotenv-cli` (devDependency). + +- Repo conventions and patterns to follow when changing code: + - Nuxt Layers: each package is a Nuxt layer with `app/`, `server/`, `translations/` or `content/` subfolders. Keep server/api logic inside the package's `server/` directory. + - UI components: shared UI in `packages/ui` uses Nuxt UI. Components exported with a `Base-` prefix (example: `BaseButton.vue`) act as proxies to upstream Nuxt UI components — preserve prefix and API when adding components. + - Package config files follow `*.config.ts` (many packages include `app.config.ts` and `nuxt.config.ts`). Prefer editing the package-level `nuxt.config.ts` when adjusting behavior for a layer. + - Layer scripts: use the root `pnpm` helpers (eg `pnpm packages ui dev`) rather than running package-local scripts directly — this preserves the workspace environment and dotenv usage. + +- Testing & CI: + - Some packages include test configs: `vitest.config.ts` and `playwright.config.ts` (see `packages/bulk-scheduler`). Run package tests using the package's npm script or `pnpm -w test` if orchestrated. + - GitHub Actions workflows live in `.github/workflows/` (e.g., `build-docker-image.yml`, `doc-deploy.yml`). Keep changes compatible with those CI steps. + +- What to change in PRs and how to format suggestions: + - Small, focused PRs that change a single layer are preferred. Update the layer `README.md` under `packages//README.md` when changing dev/start instructions. + - When suggesting code, reference the package path (example: `packages/ui/app/components/BaseButton.vue`) and prefer edits inside that package unless cross-layer changes are required. + +- Useful files to inspect for context: + - Root: `package.json` (scripts, workspace settings) — see scripts prefixed with `site`, `ui`, `db`, etc. + - `config/env.config.ts` — required environment variables. + - `README.md` — project architecture and run instructions. + - Representative layers: `packages/ui`, `packages/site`, `packages/db`, `packages/auth`. + +- Examples you can use when producing code or fixes: + - Start UI dev server: `pnpm packages ui dev` + - Run DB migrations: `pnpm packages db db:migrate` + - Build site: `pnpm packages site build` + +If anything in this document is unclear or you want more detail for a specific layer (for example, how `connect` integrates external platform adapters), tell me which layer and I will expand the instructions. +IMPORTANT: +- need to follow the rules base on the /.rules diff --git a/packages/connect/app/pages/app/business/[id].vue b/packages/connect/app/pages/app/business/[id].vue new file mode 100644 index 00000000..1e8545ff --- /dev/null +++ b/packages/connect/app/pages/app/business/[id].vue @@ -0,0 +1,470 @@ + + + + + diff --git a/packages/connect/app/pages/app/business/initial.vue b/packages/connect/app/pages/app/business/initial.vue index 1a245bfc..e0f150a5 100644 --- a/packages/connect/app/pages/app/business/initial.vue +++ b/packages/connect/app/pages/app/business/initial.vue @@ -33,7 +33,7 @@ const { businesses, getAllBusinesses } = useBusinessManager(); getAllBusinesses(); const HandleSelect = (id: string) => { - router.push(`/app/integrations`) + router.push(`/app/business/${id}`) } diff --git a/packages/connect/server/api/v1/business/[id].get.ts b/packages/connect/server/api/v1/business/[id].get.ts new file mode 100644 index 00000000..6e508a32 --- /dev/null +++ b/packages/connect/server/api/v1/business/[id].get.ts @@ -0,0 +1,34 @@ +import { businessProfileService } from '#layers/BaseDB/server/services/business-profile.service'; +import { checkUserIsLogin } from '#layers/BaseAuth/server/utils/AuthHelpers' +import { entityDetailsService } from '#layers/BaseDB/server/services/entity-details.service'; + +export default defineEventHandler(async (event) => { + const user = await checkUserIsLogin(event) + const params = getQuery(event) + const id = event.context.params?.id || params.id + if (!id || typeof id !== 'string') { + throw createError({ statusCode: 400, statusMessage: 'Missing business id' }) + } + + const result = await businessProfileService.findById(id, user.id) + if (result.error) { + throw createError({ statusCode: 404, statusMessage: result.error }) + } + + const business = result.data + + // try to load entity details for this business (optional) + try { + const details = await entityDetailsService.getDetailsByEntity(id, 'business_profile') + // attach if found + if (details) { + // @ts-ignore + business.entityDetail = details + } + } catch (err) { + // ignore missing entity details + console.error('Failed to load entity details for business', id, err) + } + + return business +}) diff --git a/packages/scheduler/server/api/v1/ai/business/engagement-targets/[id].post.ts b/packages/scheduler/server/api/v1/ai/business/engagement-targets/[id].post.ts new file mode 100644 index 00000000..3815ed28 --- /dev/null +++ b/packages/scheduler/server/api/v1/ai/business/engagement-targets/[id].post.ts @@ -0,0 +1,70 @@ +import { checkUserIsLogin } from '#layers/BaseAuth/server/utils/AuthHelpers'; +import { businessProfileService } from '#layers/BaseDB/server/services/business-profile.service'; +import { generateAIJSON } from '~/server/utils/ai'; + +export default defineEventHandler(async (event) => { + const user = await checkUserIsLogin(event); + const businessId = event.context.params?.id || getQuery(event).id; + const body = await readBody(event); + + if (!businessId || typeof businessId !== 'string') { + throw createError({ statusCode: 400, statusMessage: 'Missing business id' }); + } + + if (!body.platform || !body.keyword) { + throw createError({ statusCode: 400, statusMessage: 'Platform and keyword are required' }); + } + + const result = await businessProfileService.findById(businessId, user.id); + if (result.error || !result.data) { + throw createError({ statusCode: 404, statusMessage: 'Business not found' }); + } + + const business = result.data; + const { platform, keyword } = body; + + try { + const prompt = `Find high-engagement social media opportunities for a ${platform} business: +Business Name: ${business.name} +Industry: ${business.category} +Business Details: ${business.description} + +Search Keyword: "${keyword}" + +Generate a list of 5-10 high-engagement post opportunities that this business should target on ${platform}. +For each opportunity, provide realistic engagement metrics. + +Response JSON structure: +{ + "opportunities": [ + { + "id": "unique_id_1", + "title": "", + "engagement": "" + }, + // ... more opportunities + ] +} + +Consider: +1. Trending topics related to the keyword +2. Similar successful posts in the industry +3. User engagement patterns on ${platform} +4. Audience interests and pain points +5. Seasonal and timely opportunities + +Respond with ONLY valid JSON, no markdown code blocks.`; + + return await generateAIJSON({ + prompt, + temperature: 0.7, + maxTokens: 2000 + }); + } catch (err: any) { + console.error('Engagement targets error:', err); + throw createError({ + statusCode: 500, + statusMessage: err.message || 'Failed to search engagement targets' + }); + } +}); diff --git a/packages/scheduler/server/api/v1/ai/business/generate-blog.post.ts b/packages/scheduler/server/api/v1/ai/business/generate-blog.post.ts new file mode 100644 index 00000000..194c54a3 --- /dev/null +++ b/packages/scheduler/server/api/v1/ai/business/generate-blog.post.ts @@ -0,0 +1,49 @@ +import { checkUserIsLogin } from '#layers/BaseAuth/server/utils/AuthHelpers'; +import { generateAIJSON } from '~/server/utils/ai'; + +export default defineEventHandler(async (event) => { + const user = await checkUserIsLogin(event); + const body = await readBody(event); + + const { businessId, topic, keyword, businessDetails } = body; + + if (!businessId || !topic) { + throw createError({ statusCode: 400, statusMessage: 'Missing required fields' }); + } + + try { + const prompt = `Create an SEO-optimized blog post for a business with the following details: +Business Name: ${businessDetails?.name || 'N/A'} +Business Type: ${businessDetails?.category || 'N/A'} +Website: ${businessDetails?.website || 'N/A'} + +Topic: ${topic} +Target Keyword: ${keyword || 'general'} + +Requirements: +- Write a comprehensive blog post (500-1000 words) +- Include the target keyword naturally throughout +- Structure with proper headings +- Add a compelling introduction and conclusion +- Format the response as valid JSON with fields: title (string), content (string) + +Respond with ONLY valid JSON, no markdown code blocks.`; + + const result = await generateAIJSON({ + prompt, + temperature: 0.7, + maxTokens: 2000 + }); + + return { + title: result.title, + content: result.content + }; + } catch (err: any) { + console.error('Blog generation error:', err); + throw createError({ + statusCode: 500, + statusMessage: err.message || 'Failed to generate blog post' + }); + } +}); diff --git a/packages/scheduler/server/api/v1/ai/business/geo-audit/[id].post.ts b/packages/scheduler/server/api/v1/ai/business/geo-audit/[id].post.ts new file mode 100644 index 00000000..445424a5 --- /dev/null +++ b/packages/scheduler/server/api/v1/ai/business/geo-audit/[id].post.ts @@ -0,0 +1,68 @@ +import { checkUserIsLogin } from '#layers/BaseAuth/server/utils/AuthHelpers'; +import { businessProfileService } from '#layers/BaseDB/server/services/business-profile.service'; +import { generateAIJSON } from '~/server/utils/ai'; + +export default defineEventHandler(async (event) => { + const user = await checkUserIsLogin(event); + const businessId = event.context.params?.id || getQuery(event).id; + const body = await readBody(event); + + if (!businessId || typeof businessId !== 'string') { + throw createError({ statusCode: 400, statusMessage: 'Missing business id' }); + } + + if (!body.keyword) { + throw createError({ statusCode: 400, statusMessage: 'Keyword is required' }); + } + + const result = await businessProfileService.findById(businessId, user.id); + if (result.error || !result.data) { + throw createError({ statusCode: 404, statusMessage: 'Business not found' }); + } + + const business = result.data; + const { keyword } = body; + + try { + const prompt = `Conduct a GEO (Local Search Engine Optimization) audit for the following business: +Business Name: ${business.name} +Location/Address: ${business.address} +Industry: ${business.category} +Website: ${business.website} + +Target Local Keyword: "${keyword}" + +Analyze the business's local SEO presence and provide: +{ + "ranking": , + "actions": [ + "", + // ... 5-7 more actions + ] +} + +Focus on: +1. Google Business Profile optimization +2. Local citation consistency +3. Review management +4. Local link building +5. Local content optimization +6. NAP (Name, Address, Phone) consistency +7. Location pages and schema markup + +Each action should be specific and actionable. +Respond with ONLY valid JSON, no markdown code blocks.`; + + return await generateAIJSON({ + prompt, + temperature: 0.5, + maxTokens: 2000 + }); + } catch (err: any) { + console.error('GEO audit error:', err); + throw createError({ + statusCode: 500, + statusMessage: err.message || 'Failed to run GEO audit' + }); + } +}); diff --git a/packages/scheduler/server/api/v1/ai/business/grow-plan/[id].post.ts b/packages/scheduler/server/api/v1/ai/business/grow-plan/[id].post.ts new file mode 100644 index 00000000..a24822c7 --- /dev/null +++ b/packages/scheduler/server/api/v1/ai/business/grow-plan/[id].post.ts @@ -0,0 +1,66 @@ +import { checkUserIsLogin } from '#layers/BaseAuth/server/utils/AuthHelpers'; +import { businessProfileService } from '#layers/BaseDB/server/services/business-profile.service'; +import { generateAIJSON } from '~/server/utils/ai'; + +export default defineEventHandler(async (event) => { + const user = await checkUserIsLogin(event); + const businessId = event.context.params?.id || getQuery(event).id; + const body = await readBody(event); + + if (!businessId || typeof businessId !== 'string') { + throw createError({ statusCode: 400, statusMessage: 'Missing business id' }); + } + + const result = await businessProfileService.findById(businessId, user.id); + if (result.error || !result.data) { + throw createError({ statusCode: 404, statusMessage: 'Business not found' }); + } + + const business = result.data; + const { connections = [], posts = [] } = body; + + try { + const prompt = `Create a personalized 7-day growth strategy for a small business with these details: +Business Name: ${business.name} +Industry: ${business.category} +Website: ${business.website} +Description: ${business.description} + +Current Status: +- Connected Platforms: ${connections.length || 0} social media accounts +- Published Posts: ${posts.length || 0} + +Create a JSON response with a 7-day action plan: +{ + "days": [ + { + "day": "Day 1", + "task": "" + }, + // ... Day 2-7 + ] +} + +Focus on: +1. Content creation and optimization +2. Social media engagement +3. Audience building +4. SEO improvements +5. Community interaction + +Each task should be specific, measurable, and achievable within one day. +Respond with ONLY valid JSON, no markdown code blocks.`; + + return await generateAIJSON({ + prompt, + temperature: 0.7, + maxTokens: 2000 + }); + } catch (err: any) { + console.error('Grow plan error:', err); + throw createError({ + statusCode: 500, + statusMessage: err.message || 'Failed to generate grow plan' + }); + } +}); diff --git a/packages/scheduler/server/api/v1/ai/business/seo-audit/[id].get.ts b/packages/scheduler/server/api/v1/ai/business/seo-audit/[id].get.ts new file mode 100644 index 00000000..aa1296db --- /dev/null +++ b/packages/scheduler/server/api/v1/ai/business/seo-audit/[id].get.ts @@ -0,0 +1,55 @@ +import { checkUserIsLogin } from '#layers/BaseAuth/server/utils/AuthHelpers'; +import { businessProfileService } from '#layers/BaseDB/server/services/business-profile.service'; +import { generateAIJSON } from '~/server/utils/ai'; + +export default defineEventHandler(async (event) => { + const user = await checkUserIsLogin(event); + const businessId = event.context.params?.id || getQuery(event).id; + + if (!businessId || typeof businessId !== 'string') { + throw createError({ statusCode: 400, statusMessage: 'Missing business id' }); + } + + const result = await businessProfileService.findById(businessId, user.id); + if (result.error || !result.data) { + throw createError({ statusCode: 404, statusMessage: 'Business not found' }); + } + + const business = result.data; + + try { + const prompt = `Perform a comprehensive SEO audit for a website with these details: +Business Name: ${business.name} +Website: ${business.website} +Industry: ${business.category} +Description: ${business.description} + +Provide a JSON response with the following structure: +{ + "score": , + "issues": [], + "recommendations": [] +} + +Focus on: +1. Technical SEO (site speed, mobile-friendliness, structured data) +2. On-page SEO (meta tags, headings, keyword optimization) +3. Off-page SEO (backlinks, domain authority) +4. Content quality and freshness +5. User experience metrics + +Respond with ONLY valid JSON, no markdown code blocks.`; + + return await generateAIJSON({ + prompt, + temperature: 0.5, + maxTokens: 2000 + }); + } catch (err: any) { + console.error('SEO audit error:', err); + throw createError({ + statusCode: 500, + statusMessage: err.message || 'Failed to run SEO audit' + }); + } +}); diff --git a/packages/scheduler/server/api/v1/ai/generate.post.ts b/packages/scheduler/server/api/v1/ai/generate.post.ts index 293bebef..39c442dc 100644 --- a/packages/scheduler/server/api/v1/ai/generate.post.ts +++ b/packages/scheduler/server/api/v1/ai/generate.post.ts @@ -1,17 +1,6 @@ -import { generateText } from 'ai'; -import { google } from '@ai-sdk/google'; +import { generateAIText, generateAIJSON } from '~/server/utils/ai'; -export default defineLazyEventHandler(async () => { - const apiKey = process.env.NUXT_GOOGLE_GENERATIVE_AI_API_KEY || ''; - - if (!apiKey) { - throw createError({ - statusCode: 500, - message: 'Missing Google Generative AI API key. Please set GOOGLE_GENERATIVE_AI_API_KEY in your environment variables.', - }); - } - - return defineEventHandler(async (event) => { +export default defineEventHandler(async (event) => { const log = useLogger(event) await checkUserIsLogin(event) const body = await readBody(event); @@ -80,43 +69,50 @@ Return format: ["#hashtag1", "#hashtag2", "#hashtag3"]`; }); } - const { text } = await generateText({ - model: google('gemini-3-flash-preview'), - system: systemPrompt, - prompt: prompt, - temperature: action === 'fixGrammar' ? 0.3 : 0.7, - }); + const { result } = await handleAIGeneration( + action, + prompt, + systemPrompt, + action === 'fixGrammar' ? 0.3 : 0.7 + ); - // For actions that return JSON arrays, parse them - if (action === 'smartSplit' || action === 'generateHashtags') { - try { - // Remove markdown code blocks if present - const cleanText = text.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim(); - const parsed = JSON.parse(cleanText); - return { result: parsed }; - } catch (parseError) { - // If parsing fails, try to extract array-like content - const arrayMatch = text.match(/\[[\s\S]*\]/); - if (arrayMatch) { - try { - const parsed = JSON.parse(arrayMatch[0]); - return { result: parsed }; - } catch { - // Fallback: return as single item array - return { result: [text] }; - } - } - return { result: [text] }; - } - } - - return { result: text }; + return { result }; } catch (error: any) { console.error('AI Generation Error:', error); throw createError({ statusCode: 500, - message: error.message || 'Failed to generate AI content', + statusMessage: error.message || 'Failed to generate AI content' }); } - }); }); + +async function handleAIGeneration( + action: string, + prompt: string, + systemPrompt: string, + temperature: number +): Promise<{ result: any }> { + if (action === 'smartSplit' || action === 'generateHashtags') { + try { + const result = await generateAIJSON({ + prompt, + system: systemPrompt, + temperature + }); + return { result }; + } catch (error) { + const text = await generateAIText({ + prompt, + system: systemPrompt, + temperature + }); + return { result: [text] }; + } + } + + const text = await generateAIText({ + prompt, + system: systemPrompt, + temperature + }); + return { result: text }; diff --git a/packages/scheduler/server/utils/ai.ts b/packages/scheduler/server/utils/ai.ts new file mode 100644 index 00000000..42dbb779 --- /dev/null +++ b/packages/scheduler/server/utils/ai.ts @@ -0,0 +1,92 @@ +import { generateText } from 'ai'; +import { google } from '@ai-sdk/google'; + +export interface AIConfig { + provider?: 'google' | 'openai'; + model?: string; + apiKey?: string; + temperature?: number; + maxTokens?: number; +} + +export interface AIGenerateOptions { + prompt: string; + system?: string; + config?: AIConfig; + temperature?: number; + maxTokens?: number; +} + +type ModelProvider = ReturnType; + +function getDefaultConfig(): AIConfig { + return { + provider: 'google', + model: 'gemini-3-flash-preview', + apiKey: process.env.NUXT_GOOGLE_GENERATIVE_AI_API_KEY, + temperature: 0.7, + maxTokens: 2000 + }; +} + +function initializeModel(config: AIConfig): ModelProvider { + if (config.provider === 'google') { + if (!config.apiKey) { + const envApiKey = process.env.NUXT_GOOGLE_GENERATIVE_AI_API_KEY; + if (!envApiKey) { + throw new Error('Missing Google Generative AI API key. Please set NUXT_GOOGLE_GENERATIVE_AI_API_KEY in your environment variables.'); + } + return google(config.model || 'gemini-3-flash-preview'); + } + return google(config.model || 'gemini-3-flash-preview'); + } + + throw new Error(`Unsupported AI provider: ${config.provider}`); +} + +export async function generateAIText(options: AIGenerateOptions): Promise { + const defaultConfig = getDefaultConfig(); + const mergedConfig: AIConfig = { + ...defaultConfig, + ...options.config + }; + + const model = initializeModel(mergedConfig); + + try { + const { text } = await generateText({ + model, + system: options.system || '', + prompt: options.prompt, + temperature: options.temperature ?? mergedConfig.temperature, + maxTokens: options.maxTokens ?? mergedConfig.maxTokens + }); + + return text; + } catch (error: any) { + console.error('AI Generation Error:', error); + throw createError({ + statusCode: 500, + statusMessage: error.message || 'Failed to generate AI content' + }); + } +} + +export async function generateAIJSON(options: AIGenerateOptions): Promise { + const text = await generateAIText(options); + + try { + const cleanText = text.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim(); + return JSON.parse(cleanText); + } catch (parseError) { + const arrayMatch = text.match(/\[[\s\S]*\]/); + if (arrayMatch) { + try { + return JSON.parse(arrayMatch[0]); + } catch { + throw new Error('Failed to parse AI response as JSON'); + } + } + throw new Error('AI response is not valid JSON'); + } +}