diff --git a/.gitignore b/.gitignore index 6a5584f..6043374 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ uploads/* /Guide.md # Dedicated Documentation Folder /documents/ +/minio-seed/ \ No newline at end of file diff --git a/Channel-Backend/src/auto-index.ts b/Channel-Backend/src/auto-index.ts new file mode 100644 index 0000000..205a9de --- /dev/null +++ b/Channel-Backend/src/auto-index.ts @@ -0,0 +1,29 @@ +import dotenv from 'dotenv'; +dotenv.config(); + +import prisma from './utils/db'; +import { ChatService } from './services/chat.service'; + +async function main() { + console.log('[Auto-Indexing] Starting catalog RAG auto-indexing for restored assets...'); + const chatService = new ChatService(); + + const assets = await prisma.asset.findMany({ + where: { includeInKnowledgeBase: true }, + select: { id: true } + }); + + console.log(`[Auto-Indexing] Found ${assets.length} assets enabled for Knowledge Base.`); + const assetIds = assets.map(a => a.id); + + await chatService.autoIndexCatalog(assetIds); + + const embeddingCount = await prisma.assetEmbedding.count(); + console.log(`[Auto-Indexing] Successfully indexed ${embeddingCount} OKF vector embedding chunks into database.`); + process.exit(0); +} + +main().catch((err) => { + console.error('[Auto-Indexing] Failed:', err); + process.exit(1); +}); diff --git a/Channel-Backend/src/controllers/chat.controller.ts b/Channel-Backend/src/controllers/chat.controller.ts index 4c262d0..fbfe36b 100644 --- a/Channel-Backend/src/controllers/chat.controller.ts +++ b/Channel-Backend/src/controllers/chat.controller.ts @@ -8,17 +8,13 @@ const chatService = new ChatService(); export class ChatController { public queryChat = async (req: AuthRequest, res: Response, next: NextFunction) => { try { - const { prompt, sessionId } = req.body; - if (!prompt || !prompt.trim()) { - return res.status(400).json({ error: 'Prompt is required' }); - } - + const { prompt, sessionId, attachedEntities } = req.body; const userId = req.user?.userId; if (!userId) { return res.status(401).json({ error: 'Unauthorized' }); } - const result = await chatService.processPrompt(userId, prompt, sessionId); + const result = await chatService.processPrompt(userId, prompt || 'Analyze attached workbench items', sessionId, attachedEntities); res.status(200).json(result); } catch (err) { next(err); diff --git a/Channel-Backend/src/seed-taxonomy.ts b/Channel-Backend/src/seed-taxonomy.ts new file mode 100644 index 0000000..9b367e5 --- /dev/null +++ b/Channel-Backend/src/seed-taxonomy.ts @@ -0,0 +1,209 @@ +import dotenv from 'dotenv'; +dotenv.config(); + +import prisma from './utils/db'; + +async function seedTaxonomy() { + console.log('[Taxonomy-Seed] Starting taxonomy re-seeding with exact user specifications...'); + + await prisma.vertical.deleteMany(); + await prisma.techStack.deleteMany(); + await prisma.engagementType.deleteMany(); + await prisma.complianceStandard.deleteMany(); + + console.log('[Taxonomy-Seed] Cleared existing taxonomy tables.'); + + // 1. Group 1: Industry Verticals (11 Entries) + const verticalsData = [ + { name: 'Cybersecurity & OT Security', slug: 'cybersecurity-ot-security', icon: 'Shield', description: 'ICS/SCADA protection, threat detection, and OT security architecture.', color: '#ef4444', orderIndex: 1 }, + { name: 'Healthcare & Pharma', slug: 'healthcare-pharma', icon: 'Heart', description: 'Patient monitoring, clinical workflows, and pharma tech.', color: '#10b981', orderIndex: 2 }, + { name: 'Finance & Banking', slug: 'finance-banking', icon: 'CreditCard', description: 'Core banking systems, fraud detection, and fintech platforms.', color: '#3b82f6', orderIndex: 3 }, + { name: 'Insurance', slug: 'insurance', icon: 'ShieldCheck', description: 'InsurTech systems, claim automation, and actuarial analytics.', color: '#0284c7', orderIndex: 4 }, + { name: 'Energy & Utilities', slug: 'energy-utilities', icon: 'Zap', description: 'Grid monitoring, renewable management, and infrastructure tech.', color: '#06b6d4', orderIndex: 5 }, + { name: 'Agriculture', slug: 'agriculture', icon: 'Sprout', description: 'AgriTech telemetry, precision farming, and supply analytics.', color: '#84cc16', orderIndex: 6 }, + { name: 'Education', slug: 'education', icon: 'GraduationCap', description: 'EdTech platforms, AI tutoring, and campus management.', color: '#f59e0b', orderIndex: 7 }, + { name: 'Manufacturing & IoT', slug: 'manufacturing-iot', icon: 'Cpu', description: 'Predictive maintenance, IIoT telemetry, and smart factory tech.', color: '#8b5cf6', orderIndex: 8 }, + { name: 'Automotive', slug: 'automotive', icon: 'Car', description: 'Connected vehicles, EV telemetry, and autonomous systems.', color: '#ec4899', orderIndex: 9 }, + { name: 'Retail & Supply Chain', slug: 'retail-supply-chain', icon: 'ShoppingBag', description: 'Smart basket automation, inventory AI, and logistics.', color: '#f97316', orderIndex: 10 }, + { name: 'Blockchain', slug: 'blockchain', icon: 'Link', description: 'Distributed ledgers, smart contracts, and Web3 security.', color: '#6366f1', orderIndex: 11 }, + ]; + + const createdVerticals: Record = {}; + for (const item of verticalsData) { + const v = await prisma.vertical.create({ data: item }); + createdVerticals[item.name] = v.id; + } + + // 2. Group 2: Technology Stack (21 Entries across 4 Categories) + const techStacksData = [ + // Languages/Frameworks + { name: 'Java / Spring Boot', slug: 'java-spring-boot', category: 'Languages & Frameworks', icon: 'Code', description: 'Enterprise backend services and Spring ecosystem.', color: '#3b82f6', orderIndex: 1 }, + { name: 'Node.js', slug: 'nodejs', category: 'Languages & Frameworks', icon: 'Server', description: 'Event-driven JavaScript/TypeScript backend runtimes.', color: '#10b981', orderIndex: 2 }, + { name: 'Python', slug: 'python', category: 'Languages & Frameworks', icon: 'FileCode', description: 'Data science, AI models, and microservices.', color: '#f59e0b', orderIndex: 3 }, + { name: 'React', slug: 'react', category: 'Languages & Frameworks', icon: 'Layout', description: 'Modern web component UIs and frontend state management.', color: '#06b6d4', orderIndex: 4 }, + { name: 'Go', slug: 'golang', category: 'Languages & Frameworks', icon: 'Cpu', description: 'High-performance cloud-native microservices.', color: '#0284c7', orderIndex: 5 }, + { name: '.NET', slug: 'dotnet', category: 'Languages & Frameworks', icon: 'Layers', description: 'C# enterprise applications and Microsoft ecosystem.', color: '#6366f1', orderIndex: 6 }, + + // AI/ML + { name: 'AI & ML', slug: 'ai-ml', category: 'AI & ML', icon: 'Sparkles', description: 'Core artificial intelligence and machine learning models.', color: '#8b5cf6', orderIndex: 7 }, + { name: 'LLM / Agentic', slug: 'llm-agentic', category: 'AI & ML', icon: 'Bot', description: 'Large language models, multi-agent frameworks, and reasoning engines.', color: '#a855f7', orderIndex: 8 }, + { name: 'RAG', slug: 'rag', category: 'AI & ML', icon: 'Database', description: 'Retrieval-Augmented Generation and vector search systems.', color: '#ec4899', orderIndex: 9 }, + { name: 'Computer Vision', slug: 'computer-vision', category: 'AI & ML', icon: 'Camera', description: 'Real-time video analytics and optical recognition.', color: '#f43f5e', orderIndex: 10 }, + { name: 'ML Pipelines', slug: 'ml-pipelines', category: 'AI & ML', icon: 'GitBranch', description: 'MLOps, model retraining, and feature stores.', color: '#d946ef', orderIndex: 11 }, + { name: 'Deepfake / Detection', slug: 'deepfake-detection', category: 'AI & ML', icon: 'Eye', description: 'Synthetic media verification and anti-spoofing.', color: '#ef4444', orderIndex: 12 }, + + // Data/Backend + { name: 'PostgreSQL', slug: 'postgresql', category: 'Data & Backend', icon: 'Database', description: 'Relational database with JSONB and vector capabilities.', color: '#3b82f6', orderIndex: 13 }, + { name: 'Temporal', slug: 'temporal', category: 'Data & Backend', icon: 'Clock', description: 'Durable workflow execution and saga orchestrations.', color: '#10b981', orderIndex: 14 }, + { name: 'Kafka', slug: 'kafka', category: 'Data & Backend', icon: 'Activity', description: 'Distributed event streaming and message pub/sub.', color: '#f59e0b', orderIndex: 15 }, + { name: 'Event-driven', slug: 'event-driven', category: 'Data & Backend', icon: 'Zap', description: 'Asynchronous event architecture and CQRS patterns.', color: '#06b6d4', orderIndex: 16 }, + { name: 'Microservices', slug: 'microservices', category: 'Data & Backend', icon: 'Grid', description: 'Decoupled service APIs and domain-driven design.', color: '#6366f1', orderIndex: 17 }, + + // Cloud/Infra + { name: 'AWS', slug: 'aws', category: 'Cloud & Infra', icon: 'Cloud', description: 'Amazon Web Services cloud infrastructure.', color: '#f97316', orderIndex: 18 }, + { name: 'Sovereign / On-prem', slug: 'sovereign-onprem', category: 'Cloud & Infra', icon: 'Lock', description: 'Sovereign cloud hosting and air-gapped on-premise deployments.', color: '#64748b', orderIndex: 19 }, + { name: 'Kubernetes', slug: 'kubernetes', category: 'Cloud & Infra', icon: 'Box', description: 'K8s container orchestration and mesh networking.', color: '#0284c7', orderIndex: 20 }, + { name: 'IaaS', slug: 'iaas', category: 'Cloud & Infra', icon: 'Server', description: 'Infrastructure-as-a-Service and virtualized bare metal.', color: '#475569', orderIndex: 21 }, + ]; + + const createdTechStacks: Record = {}; + for (const item of techStacksData) { + const ts = await prisma.techStack.create({ data: item }); + createdTechStacks[item.name] = ts.id; + } + + // 3. Group 3: Engagement Type (4 Entries) + const engagementTypesData = [ + { name: 'Build', slug: 'build', icon: 'Wrench', description: 'Greenfield product engineering and 0-to-1 development.', color: '#3b82f6', orderIndex: 1 }, + { name: 'Rescue', slug: 'rescue', icon: 'LifeBuoy', description: 'Turnaround engineering, legacy modernization, and critical fixes.', color: '#ef4444', orderIndex: 2 }, + { name: 'Scale', slug: 'scale', icon: 'TrendingUp', description: 'Performance optimization, architecture scaling, and throughput expansion.', color: '#10b981', orderIndex: 3 }, + { name: 'Due Diligence', slug: 'due-diligence', icon: 'FileSearch', description: 'Technical audits, code reviews, and M&A architecture assessments.', color: '#f59e0b', orderIndex: 4 }, + ]; + + const createdEngagementTypes: Record = {}; + for (const item of engagementTypesData) { + const et = await prisma.engagementType.create({ data: item }); + createdEngagementTypes[item.name] = et.id; + } + + // 4. Group 4: Compliance / Regulatory (5 Entries) + const complianceStandardsData = [ + { name: 'HIPAA', slug: 'hipaa', icon: 'Activity', description: 'Health Insurance Portability and Accountability Act.', color: '#ec4899', orderIndex: 1 }, + { name: 'GxP', slug: 'gxp', icon: 'ShieldCheck', description: 'Good Practice quality guidelines for pharma and life sciences.', color: '#10b981', orderIndex: 2 }, + { name: 'APRA CPS 230', slug: 'apra-cps-230', icon: 'Building', description: 'APRA Operational Risk Management standard for banking.', color: '#3b82f6', orderIndex: 3 }, + { name: 'SOC 2', slug: 'soc-2', icon: 'FileCheck', description: 'SOC 2 security, availability, and confidentiality controls.', color: '#06b6d4', orderIndex: 4 }, + { name: 'GDPR / Sovereign', slug: 'gdpr-sovereign', icon: 'Lock', description: 'EU General Data Protection Regulation and data sovereignty.', color: '#8b5cf6', orderIndex: 5 }, + ]; + + const createdCompliance: Record = {}; + for (const item of complianceStandardsData) { + const cs = await prisma.complianceStandard.create({ data: item }); + createdCompliance[item.name] = cs.id; + } + + console.log('[Taxonomy-Seed] Successfully seeded 11 Verticals, 21 Tech Stacks, 4 Engagement Types, and 5 Compliance Standards.'); + + // 5. Re-map Catalog Assets to the Exact Taxonomy Entries + const assets = await prisma.asset.findMany(); + console.log(`[Taxonomy-Seed] Mapping exact taxonomy relations for ${assets.length} catalog assets...`); + + let updatedCount = 0; + + for (const asset of assets) { + const text = (asset.title + ' ' + (asset.description || '') + ' ' + (asset.tags || []).join(' ')).toLowerCase(); + + const targetVerticals: string[] = []; + const targetTechs: string[] = []; + const targetEngagements: string[] = []; + const targetCompliance: string[] = []; + + // Verticals mapping + if (text.includes('cyber') || text.includes('security') || text.includes('scada') || text.includes('ot security')) { + if (createdVerticals['Cybersecurity & OT Security']) targetVerticals.push(createdVerticals['Cybersecurity & OT Security']); + } + if (text.includes('health') || text.includes('patient') || text.includes('medical') || text.includes('pharma') || text.includes('diabetic')) { + if (createdVerticals['Healthcare & Pharma']) targetVerticals.push(createdVerticals['Healthcare & Pharma']); + } + if (text.includes('bank') || text.includes('finance') || text.includes('fintech') || text.includes('payment')) { + if (createdVerticals['Finance & Banking']) targetVerticals.push(createdVerticals['Finance & Banking']); + } + if (text.includes('insurance') || text.includes('claim')) { + if (createdVerticals['Insurance']) targetVerticals.push(createdVerticals['Insurance']); + } + if (text.includes('energy') || text.includes('grid') || text.includes('metering') || text.includes('utility') || text.includes('water')) { + if (createdVerticals['Energy & Utilities']) targetVerticals.push(createdVerticals['Energy & Utilities']); + } + if (text.includes('agri') || text.includes('farm') || text.includes('crop')) { + if (createdVerticals['Agriculture']) targetVerticals.push(createdVerticals['Agriculture']); + } + if (text.includes('student') || text.includes('education') || text.includes('textbook') || text.includes('school') || text.includes('plagiarism')) { + if (createdVerticals['Education']) targetVerticals.push(createdVerticals['Education']); + } + if (text.includes('manufactur') || text.includes('iot') || text.includes('sensor') || text.includes('factory')) { + if (createdVerticals['Manufacturing & IoT']) targetVerticals.push(createdVerticals['Manufacturing & IoT']); + } + if (text.includes('auto') || text.includes('vehicle') || text.includes('car') || text.includes('ev ')) { + if (createdVerticals['Automotive']) targetVerticals.push(createdVerticals['Automotive']); + } + if (text.includes('retail') || text.includes('basket') || text.includes('store') || text.includes('supply')) { + if (createdVerticals['Retail & Supply Chain']) targetVerticals.push(createdVerticals['Retail & Supply Chain']); + } + if (text.includes('blockchain') || text.includes('ledger') || text.includes('web3')) { + if (createdVerticals['Blockchain']) targetVerticals.push(createdVerticals['Blockchain']); + } + + // Tech Stacks mapping + if (text.includes('ai') || text.includes('ml') || text.includes('model') || text.includes('predictive') || text.includes('chatbot')) { + if (createdTechStacks['AI & ML']) targetTechs.push(createdTechStacks['AI & ML']); + } + if (text.includes('llm') || text.includes('agent') || text.includes('gpt') || text.includes('deepseek')) { + if (createdTechStacks['LLM / Agentic']) targetTechs.push(createdTechStacks['LLM / Agentic']); + } + if (text.includes('rag') || text.includes('vector') || text.includes('retrieval')) { + if (createdTechStacks['RAG']) targetTechs.push(createdTechStacks['RAG']); + } + if (text.includes('vision') || text.includes('camera') || text.includes('image')) { + if (createdTechStacks['Computer Vision']) targetTechs.push(createdTechStacks['Computer Vision']); + } + if (text.includes('postgres') || text.includes('db') || text.includes('sql')) { + if (createdTechStacks['PostgreSQL']) targetTechs.push(createdTechStacks['PostgreSQL']); + } + if (text.includes('aws') || text.includes('cloud') || text.includes('server')) { + if (createdTechStacks['AWS']) targetTechs.push(createdTechStacks['AWS']); + } + + // Default Fallbacks + if (targetVerticals.length === 0 && createdVerticals['Cybersecurity & OT Security']) { + targetVerticals.push(createdVerticals['Cybersecurity & OT Security']); + } + if (targetTechs.length === 0 && createdTechStacks['AI & ML']) { + targetTechs.push(createdTechStacks['AI & ML']); + } + if (createdEngagementTypes['Build']) { + targetEngagements.push(createdEngagementTypes['Build']); + } + if (createdCompliance['SOC 2']) { + targetCompliance.push(createdCompliance['SOC 2']); + } + + await prisma.asset.update({ + where: { id: asset.id }, + data: { + verticals: { connect: targetVerticals.map(id => ({ id })) }, + techStacks: { connect: targetTechs.map(id => ({ id })) }, + engagementTypes: { connect: targetEngagements.map(id => ({ id })) }, + complianceStandards: { connect: targetCompliance.map(id => ({ id })) }, + } + }); + + updatedCount++; + } + + console.log(`[Taxonomy-Seed] Successfully mapped exact taxonomy relations for ${updatedCount} assets.`); + process.exit(0); +} + +seedTaxonomy().catch(err => { + console.error('[Taxonomy-Seed] Failed:', err); + process.exit(1); +}); diff --git a/Channel-Backend/src/services/chat.service.ts b/Channel-Backend/src/services/chat.service.ts index a1914b2..42cd396 100644 --- a/Channel-Backend/src/services/chat.service.ts +++ b/Channel-Backend/src/services/chat.service.ts @@ -19,7 +19,23 @@ export class ChatService { /** * Process a user chat prompt with RBAC-scoped, unified RAG retrieval across Assets, Showcase Reels, Ecosystem Offerings, and Legal Documents. */ - public async processPrompt(userId: string, prompt: string, sessionId?: string) { + public async processPrompt( + userId: string, + prompt: string, + sessionId?: string, + attachedEntities?: Array<{ + id: string; + title: string; + type: string; + entityKind: 'ASSET' | 'SHOWCASE' | 'ECOSYSTEM' | 'LEGAL'; + url?: string; + description?: string; + problemStatement?: string; + solution?: string; + thumbnailUrl?: string; + tags?: string[]; + }> + ) { const user = await prisma.user.findUnique({ where: { id: userId }, include: { @@ -99,7 +115,10 @@ export class ChatService { description: true, problemStatement: true, solution: true, - assetGroups: { select: { name: true } } + assetGroups: { select: { name: true } }, + verticals: { select: { name: true } }, + techStacks: { select: { name: true } }, + complianceStandards: { select: { name: true } }, } } } @@ -125,123 +144,257 @@ export class ChatService { const citationsMap = new Map(); const contextLines: string[] = []; - // A. Match ContentShowcase items (e.g., "digitaltwin", "digital twin", "aura", "smart basket", "jonas blue") - showcaseItems.forEach((sc) => { - const fullText = (sc.title + ' ' + (sc.description || '')).toLowerCase(); - const textNorm = normalizeStr(fullText); + const hasAttachedEntities = attachedEntities && attachedEntities.length > 0; - let matchCount = 0; - promptTerms.forEach(term => { - const termNorm = normalizeStr(term); - if (termNorm && (fullText.includes(term) || textNorm.includes(termNorm))) { - matchCount++; + // Explicit Attached Entities Ingestion (Drag-and-Drop AI Workbench) + if (hasAttachedEntities) { + for (const ent of attachedEntities!) { + if (ent.entityKind === 'ASSET') { + const dbAsset = await prisma.asset.findUnique({ + where: { id: ent.id }, + include: { + verticals: true, + techStacks: true, + complianceStandards: true, + } + }); + + if (dbAsset) { + citationsMap.set(dbAsset.id, { + assetId: dbAsset.id, + title: dbAsset.title, + location: 'Inspected Catalog Asset', + type: dbAsset.type, + isRecommended: false, + }); + + contextLines.push( + `[WORKBENCH ATTACHED ASSET] Title: "${dbAsset.title}" | Type: "${dbAsset.type}" | Link/URL: "${dbAsset.url}" | Category: "${dbAsset.categoryId || 'General'}" | Subcategory: "${dbAsset.subcategory || '-'}" | Description: "${dbAsset.description || 'N/A'}" | Problem Statement: "${dbAsset.problemStatement || 'N/A'}" | Solution Overview: "${dbAsset.solution || 'N/A'}" | Industry Verticals: "${(dbAsset.verticals || []).map(v => v.name).join(', ')}" | Tech Stack: "${(dbAsset.techStacks || []).map(t => t.name).join(', ')}" | Compliance Standards: "${(dbAsset.complianceStandards || []).map(c => c.name).join(', ')}"\n` + ); + } + } else if (ent.entityKind === 'SHOWCASE') { + const dbShowcase = await prisma.contentShowcase.findUnique({ + where: { id: ent.id } + }); + + if (dbShowcase) { + citationsMap.set(dbShowcase.id, { + assetId: dbShowcase.id, + title: dbShowcase.title, + location: 'Featured Content Showcase', + type: 'case_study', + isRecommended: false, + }); + + contextLines.push( + `[WORKBENCH ATTACHED FEATURED REEL] Title: "${dbShowcase.title}" | Video URL: "${dbShowcase.youtubeUrl}" | Description:\n${dbShowcase.description || 'Interactive product reel'}\n` + ); + } + } else if (ent.entityKind === 'ECOSYSTEM') { + const dbOffering = await prisma.ecosystemOffering.findUnique({ + where: { id: ent.id } + }); + + if (dbOffering) { + citationsMap.set(dbOffering.id, { + assetId: dbOffering.id, + title: dbOffering.name, + location: 'Ecosystem Offering', + type: 'offering', + isRecommended: false, + }); + + contextLines.push( + `[WORKBENCH ATTACHED ECOSYSTEM OFFERING] Name: "${dbOffering.name}" | Type: "${dbOffering.type}" | Tagline: "${dbOffering.tagline}" | Website URL: "${dbOffering.websiteUrl}" | Description:\n${dbOffering.description}\n` + ); + } + } else if (ent.entityKind === 'LEGAL') { + const dbLegal = await prisma.legalDocument.findFirst({ + where: { OR: [{ id: ent.id }, { type: ent.title.includes('NDA') ? 'NDA' : 'MSA' }] } + }); + + if (dbLegal) { + citationsMap.set(dbLegal.id, { + assetId: dbLegal.id, + title: ent.title, + location: 'Legal Agreement', + type: 'legal', + isRecommended: false, + }); + + contextLines.push( + `[WORKBENCH ATTACHED LEGAL AGREEMENT] Title: "${ent.title}" | Version: "${dbLegal.version}" | Content Summary:\n${dbLegal.content.slice(0, 800)}...\n` + ); + } } - }); - - const isDirectMatch = (promptNorm.length >= 4 && textNorm.includes(promptNorm)) || - promptLower.includes(sc.title.toLowerCase()) || - sc.title.toLowerCase().includes(promptLower) || - (promptNorm.includes('digitaltwin') && textNorm.includes('digitaltwin')) || - (promptNorm.includes('aura') && textNorm.includes('aura')) || - (promptNorm.includes('smartbasket') && textNorm.includes('smartbasket')) || - (promptNorm.includes('jonasblue') && textNorm.includes('jonasblue')); - - if (isDirectMatch || matchCount >= 1) { - citationsMap.set(sc.id, { - assetId: sc.id, - title: sc.title, - location: 'Featured Content Showcase', - type: 'case_study', - isRecommended: false, - }); - - contextLines.push( - `[Featured Content Reel] Title: "${sc.title}" | ID: "${sc.id}" | URL: "${sc.youtubeUrl}" | Description:\n${sc.description || 'Interactive product reel'}\n` - ); } - }); + } else { + // ONLY RUN RAG WHEN NO ENTITIES ARE ATTACHED - // B. Match EcosystemOffering items - ecosystemOfferings.forEach((eo) => { - const fullText = (eo.name + ' ' + eo.tagline + ' ' + eo.description).toLowerCase(); - const textNorm = normalizeStr(fullText); + // A. Match ContentShowcase items (Only on explicit query or exact title match) + const isExplicitShowcaseQuery = promptLower.includes('showcase') || promptLower.includes('video') || promptLower.includes('reel') || promptLower.includes('featured content'); - let matchCount = 0; - promptTerms.forEach(term => { - const termNorm = normalizeStr(term); - if (termNorm && (fullText.includes(term) || textNorm.includes(termNorm))) { - matchCount++; - } - }); + showcaseItems.forEach((sc) => { + const fullText = (sc.title + ' ' + (sc.description || '')).toLowerCase(); + const textNorm = normalizeStr(fullText); - const isDirectMatch = (promptNorm.length >= 4 && textNorm.includes(promptNorm)) || - promptLower.includes(eo.name.toLowerCase()) || - eo.name.toLowerCase().includes(promptLower); - - if (isDirectMatch || matchCount >= 1) { - citationsMap.set(eo.id, { - assetId: eo.id, - title: eo.name, - location: 'Ecosystem Offering', - type: 'offering', - isRecommended: false, - }); - - contextLines.push( - `[Ecosystem Offering] Name: "${eo.name}" | Type: "${eo.type}" | Tagline: "${eo.tagline}" | Website: "${eo.websiteUrl}" | Description:\n${eo.description}\n` - ); - } - }); - - // C. Hybrid Vector & Keyword Search on Catalog Assets - const isPureNavPrompt = promptLower.includes('theme') || promptLower.includes('dark mode') || promptLower.includes('light mode') || promptLower.includes('how to change') || promptLower.includes('appearance'); - - if (!isPureNavPrompt) { - const scoredChunks = embeddings.map(emb => { - let vectorScore = 0; - try { - const vec = JSON.parse(emb.vector) as number[]; - vectorScore = promptVector.reduce((acc: number, val: number, i: number) => acc + val * (vec[i] || 0), 0); - } catch { - vectorScore = 0; - } - - const chunkText = (emb.content + ' ' + emb.asset.title + ' ' + (emb.asset.description || '')).toLowerCase(); - let keywordMatches = 0; + let matchCount = 0; promptTerms.forEach(term => { - if (chunkText.includes(term)) keywordMatches += 1; + const termNorm = normalizeStr(term); + if (termNorm && (fullText.includes(term) || textNorm.includes(termNorm))) { + matchCount++; + } }); - const hybridScore = vectorScore * 0.5 + (keywordMatches / Math.max(promptTerms.length, 1)) * 0.5; - return { chunk: emb, score: hybridScore, keywordMatches }; - }).sort((a, b) => b.score - a.score); + const isExactTitleMatch = promptLower.includes(sc.title.toLowerCase()) || sc.title.toLowerCase().includes(promptLower); - const topAssetChunks = scoredChunks.filter(({ score, keywordMatches }) => score >= 0.15 || keywordMatches > 0).slice(0, 3); + if (isExactTitleMatch || (isExplicitShowcaseQuery && matchCount >= 2)) { + citationsMap.set(sc.id, { + assetId: sc.id, + title: sc.title, + location: 'Featured Content Showcase', + type: 'case_study', + isRecommended: false, + }); - topAssetChunks.forEach(({ chunk }, idx) => { - const meta = (chunk.sourceMetadata as unknown as OKFMetadata) || { - assetId: chunk.assetId, - assetTitle: chunk.asset.title, - assetType: chunk.asset.type, - location: `Segment ${chunk.chunkIndex + 1}`, - }; - - const isRecommended = partnerGroup - ? chunk.asset.assetGroups.some(g => g.name.trim().toLowerCase() === partnerGroup) - : false; - - citationsMap.set(chunk.assetId, { - assetId: chunk.assetId, - title: chunk.asset.title, - location: meta.location, - type: chunk.asset.type, - isRecommended, - }); - - contextLines.push( - `[Catalog Asset ${idx + 1}] Title: "${chunk.asset.title}" | ID: "${chunk.assetId}" | Location: "${meta.location}" | Details:\n${chunk.content}\n` - ); + contextLines.push( + `[Featured Content Reel] Title: "${sc.title}" | ID: "${sc.id}" | URL: "${sc.youtubeUrl}" | Description:\n${sc.description || 'Interactive product reel'}\n` + ); + } }); + + // B. Match EcosystemOffering items (Only on explicit query or exact name match) + const isExplicitEcosystemQuery = promptLower.includes('ecosystem') || promptLower.includes('offering') || promptLower.includes('partner product') || promptLower.includes('explore more'); + + ecosystemOfferings.forEach((eo) => { + const fullText = (eo.name + ' ' + eo.tagline + ' ' + eo.description).toLowerCase(); + const textNorm = normalizeStr(fullText); + + let matchCount = 0; + promptTerms.forEach(term => { + const termNorm = normalizeStr(term); + if (termNorm && (fullText.includes(term) || textNorm.includes(termNorm))) { + matchCount++; + } + }); + + const isExactNameMatch = promptLower.includes(eo.name.toLowerCase()) || eo.name.toLowerCase().includes(promptLower); + + if (isExactNameMatch || (isExplicitEcosystemQuery && matchCount >= 2)) { + citationsMap.set(eo.id, { + assetId: eo.id, + title: eo.name, + location: 'Ecosystem Offering', + type: 'offering', + isRecommended: false, + }); + + contextLines.push( + `[Ecosystem Offering] Name: "${eo.name}" | Type: "${eo.type}" | Tagline: "${eo.tagline}" | Website: "${eo.websiteUrl}" | Description:\n${eo.description}\n` + ); + } + }); + + // C. Direct Catalog Asset Title/Description/Taxonomy Search + const catalogAssets = await prisma.asset.findMany({ + where: { + id: { in: accessibleAssetIds }, + status: 'published', + }, + include: { + assetGroups: { select: { name: true } }, + verticals: { select: { name: true } }, + techStacks: { select: { name: true } }, + complianceStandards: { select: { name: true } }, + } + }); + + const stopWords = new Set(['there', 'about', 'where', 'which', 'what', 'have', 'with', 'from', 'this', 'that', 'your', 'portal', 'asset', 'assets', 'product', 'item', 'these', 'those', 'please', 'explain', 'tell']); + const keyTerms = promptTerms.filter(t => !stopWords.has(t)); + + if (keyTerms.length > 0) { + catalogAssets.forEach(a => { + const fullText = (a.title + ' ' + (a.description || '') + ' ' + (a.tags || []).join(' ') + ' ' + (a.verticals || []).map(v => v.name).join(' ') + ' ' + (a.techStacks || []).map(t => t.name).join(' ')).toLowerCase(); + + let matchCount = 0; + keyTerms.forEach(kt => { + if (fullText.includes(kt)) matchCount++; + }); + + if (matchCount >= 1) { + const isRecommended = partnerGroup + ? a.assetGroups.some(g => g.name.trim().toLowerCase() === partnerGroup) + : false; + + if (!citationsMap.has(a.id)) { + citationsMap.set(a.id, { + assetId: a.id, + title: a.title, + location: 'Catalog Asset Overview', + type: a.type, + isRecommended, + }); + + contextLines.push( + `[Direct Catalog Asset Match] Title: "${a.title}" | ID: "${a.id}" | Type: "${a.type}" | Description: "${a.description || ''}" | Problem: "${a.problemStatement || ''}" | Solution: "${a.solution || ''}"\n` + ); + } + } + }); + } + + // D. Hybrid Vector Search (Only if key terms present) + const isPureNavPrompt = promptLower.includes('theme') || promptLower.includes('dark mode') || promptLower.includes('light mode') || promptLower.includes('how to change') || promptLower.includes('appearance'); + + if (!isPureNavPrompt && keyTerms.length > 0) { + const scoredChunks = embeddings.map(emb => { + let vectorScore = 0; + try { + const vec = JSON.parse(emb.vector) as number[]; + vectorScore = promptVector.reduce((acc: number, val: number, i: number) => acc + val * (vec[i] || 0), 0); + } catch { + vectorScore = 0; + } + + const chunkText = (emb.content + ' ' + emb.asset.title + ' ' + (emb.asset.description || '')).toLowerCase(); + let keywordMatches = 0; + keyTerms.forEach(term => { + if (chunkText.includes(term)) keywordMatches += 1; + }); + + const hybridScore = vectorScore * 0.5 + (keywordMatches / Math.max(keyTerms.length, 1)) * 0.5; + return { chunk: emb, score: hybridScore, keywordMatches }; + }).sort((a, b) => b.score - a.score); + + const topAssetChunks = scoredChunks.filter(({ score, keywordMatches }) => score >= 0.35 && keywordMatches >= 1).slice(0, 3); + + topAssetChunks.forEach(({ chunk }, idx) => { + const meta = (chunk.sourceMetadata as unknown as OKFMetadata) || { + assetId: chunk.assetId, + assetTitle: chunk.asset.title, + assetType: chunk.asset.type, + location: `Segment ${chunk.chunkIndex + 1}`, + }; + + const isRecommended = partnerGroup + ? chunk.asset.assetGroups.some(g => g.name.trim().toLowerCase() === partnerGroup) + : false; + + if (!citationsMap.has(chunk.assetId)) { + citationsMap.set(chunk.assetId, { + assetId: chunk.assetId, + title: chunk.asset.title, + location: meta.location, + type: chunk.asset.type, + isRecommended, + }); + } + + contextLines.push( + `[Catalog Asset ${idx + 1}] Title: "${chunk.asset.title}" | ID: "${chunk.assetId}" | Location: "${meta.location}" | Details:\n${chunk.content}\n` + ); + }); + } } // D. Inject Assigned Legal Documents (NDA / MSA) if prompt asks about NDA / Legal @@ -290,26 +443,103 @@ Exact Admin Console Layout Guide for Administrator (${user.email}): 5. Ecosystem Manager (/admin/ecosystem): Left Sidebar -> "Ecosystem Manager". `; - const systemPrompt = `You are Tech4Biz AI Advisor, the official enterprise assistant for the Channel Partner Portal. -Your task is to provide accurate, professional, and clear answers to user questions. + const systemPrompt = `You are Tech4Biz AI Advisor Workbench, the official enterprise assistant for the Channel Partner Portal. -Role & Context: +Role & Target Audience: - User: ${user.email} (${isClient ? 'Client / Partner' : 'Portal Administrator'}). -- STIPULATION: NEVER tell a Client user to look for "Legal Agreements" in the left sidebar. State clearly: "Click the 'Legal Agreements' card on your Home Dashboard or go to /client/agreements". -- STIPULATION: NEVER mention admin console routes (/admin, Partner Directory, Legal Templates) when talking to a Client user. -Knowledge Guidelines: -- If the user asks about a specific Featured Content video or Case Study (such as "digital twin", "digitaltwin", "where is digital twin", "what is digital twin", "smart basket", "aura", "jonas blue"), identify the exact showcase item from Knowledge Context and summarize its title, description, and YouTube link. -- Answer catalog/product questions using the provided Catalog Asset Snippets. -- For navigation or portal help, use the exact Client Portal Layout & Step-by-Step Navigation Guide. -- Use bold headers and clear formatting in Markdown.`; +OUTPUT FORMATTING REQUIREMENTS (CRITICAL FOR IMMACULATE VISUAL STRUCTURE): +1. **Multi-Turn Context Awareness**: Maintain full conversational memory. When asked for follow-ups or comparisons of previously mentioned assets, resolve references accurately. +2. **Never Output Concatenated Single-Line Tables**: EVERY Markdown table row MUST be separated by a real newline character (\n). Never concatenate table rows like "| Col A | Col B | | :--- | :--- |". +3. **Structure Sections Clearly**: Use bold section titles (e.g. "### Summary", "### Key Differences", "### Features & Specifications"). +4. **Use Bullet Points for Readability**: When detailing lists of features, target users, or tech stacks, use bulleted lists instead of long unformatted paragraphs. +5. **Clickable Links**: Include direct clickable Markdown links if URLs or resources exist (e.g., "šŸ”— **Direct Link**: [Visit Resource](URL)"). + +${hasAttachedEntities ? ` +CRITICAL RULES FOR WORKBENCH ATTACHED ENTITIES: +- THE USER HAS EXCLUSIVELY ATTACHED SPECIFIC ENTITIES TO INSPECT: ${attachedEntities?.map(e => `"${e.title}"`).join(', ')}. +- You MUST answer ONLY about the attached items listed under [WORKBENCH ATTACHED ASSET], [WORKBENCH ATTACHED FEATURED REEL], [WORKBENCH ATTACHED ECOSYSTEM OFFERING], or [WORKBENCH ATTACHED LEGAL AGREEMENT]. +- Provide a clear, high-impact summary of what these attached items are, their core problem/solution, tech stack, and key features. +- Do NOT list, summarize, or invent any unattached showcase reels (like Digital Twin, Smart Basket, etc.) or unrelated catalog assets! Focus 100% EXCLUSIVELY on the attached items. +` : ` +- Answer user questions accurately using the provided Knowledge Context. +- Do NOT hallucinate or list random showcase reels or unrelated assets unless explicitly requested by the user. +`} + +Portal Navigation Guidelines: +- STIPULATION: NEVER tell a Client user to look for "Legal Agreements" in the left sidebar. State clearly: "Click the 'Legal Agreements' card on your Home Dashboard or go to /client/agreements". +- STIPULATION: NEVER mention admin console routes (/admin, Partner Directory, Legal Templates) when talking to a Client user.`; + + // 0. Fetch Past Conversation History for Session Context & Follow-up Resolution + let pastMessages: { role: string; content: string }[] = []; + let activeSessionId = sessionId; + + if (!activeSessionId) { + const session = await prisma.chatSession.create({ + data: { userId } + }); + activeSessionId = session.id; + } else { + const dbPast = await prisma.chatMessage.findMany({ + where: { sessionId: activeSessionId }, + orderBy: { createdAt: 'asc' }, + take: 12, + }); + + pastMessages = dbPast.map(m => ({ + role: m.sender === 'USER' ? 'user' : 'assistant', + content: m.content + })); + + // Extract citations / asset references from recent ASSISTANT messages to handle follow-up queries like "provide more details on this asset" + const recentBotMsgs = dbPast.filter(m => m.sender === 'ASSISTANT' && m.citations); + for (const botMsg of recentBotMsgs) { + const cites = (botMsg.citations as unknown as CitationItem[]) || []; + for (const cite of cites) { + if (cite.assetId && !citationsMap.has(cite.assetId)) { + const dbAsset = await prisma.asset.findUnique({ + where: { id: cite.assetId }, + include: { + verticals: true, + techStacks: true, + complianceStandards: true, + } + }); + + if (dbAsset) { + citationsMap.set(dbAsset.id, { + assetId: dbAsset.id, + title: dbAsset.title, + location: 'Previously Discussed Asset', + type: dbAsset.type, + isRecommended: false, + }); + + contextLines.push( + `[PREVIOUSLY DISCUSSED ASSET IN CONVERSATION] Title: "${dbAsset.title}" | Type: "${dbAsset.type}" | Link/URL: "${dbAsset.url}" | Category: "${dbAsset.categoryId || 'General'}" | Description: "${dbAsset.description || 'N/A'}" | Problem Statement: "${dbAsset.problemStatement || 'N/A'}" | Solution Overview: "${dbAsset.solution || 'N/A'}" | Industry Verticals: "${(dbAsset.verticals || []).map(v => v.name).join(', ')}" | Tech Stack: "${(dbAsset.techStacks || []).map(t => t.name).join(', ')}"\n` + ); + } + } + } + } + } + + // Save current User prompt first to maintain chronological integrity + await prisma.chatMessage.create({ + data: { + sessionId: activeSessionId, + sender: 'USER', + content: prompt, + } + }); const messages = [ { role: 'system', content: systemPrompt }, + ...pastMessages, { role: 'user', content: `Platform Navigation Guide:\n${portalGuideContext}\n\nKnowledge Context:\n${contextBlock}\n\nUser Question: ${prompt}` } ]; - // 5. Call DeepSeek LLM Gateway API + // 6. Call DeepSeek LLM Gateway API let assistantReply = ''; try { const response = await axios.post(`${this.gatewayUrl}/chat/completions`, { @@ -329,38 +559,26 @@ Knowledge Guidelines: assistantReply = 'Here is the requested information from your shared catalog:\n\n' + contextBlock; } - const citations = Array.from(citationsMap.values()); + // Strict Citation Filtering: Only include Verified Knowledge Source citations if user explicitly attached entities or asked for asset recommendations + const isExplicitSearchQuery = promptLower.includes('find asset') || promptLower.includes('search asset') || promptLower.includes('show asset') || promptLower.includes('recommend asset') || promptLower.includes('showcase video') || promptLower.includes('legal document'); - // 6. Save Chat Session & Messages - let activeSessionId = sessionId; - if (!activeSessionId) { - const session = await prisma.chatSession.create({ - data: { userId } - }); - activeSessionId = session.id; - } - - await prisma.chatMessage.create({ - data: { - sessionId: activeSessionId, - sender: 'USER', - content: prompt, - } - }); + const finalCitations = (hasAttachedEntities || isExplicitSearchQuery) + ? Array.from(citationsMap.values()) + : []; const botMessage = await prisma.chatMessage.create({ data: { sessionId: activeSessionId, sender: 'ASSISTANT', content: assistantReply, - citations: citations as any, + citations: finalCitations as any, } }); return { sessionId: activeSessionId, message: botMessage, - citations, + citations: finalCitations, }; } diff --git a/Channel-Backend/src/services/extraction.service.ts b/Channel-Backend/src/services/extraction.service.ts index e4e78c1..9788518 100644 --- a/Channel-Backend/src/services/extraction.service.ts +++ b/Channel-Backend/src/services/extraction.service.ts @@ -1,6 +1,7 @@ import fs from 'fs'; import path from 'path'; -const pdfParse = require('pdf-parse'); +const _pdfParse = require('pdf-parse'); +const pdfParse = _pdfParse.PDFParse || _pdfParse.default || _pdfParse; import mammoth from 'mammoth'; import * as XLSX from 'xlsx'; import * as cheerio from 'cheerio'; @@ -68,6 +69,19 @@ export class ExtractionService { const chunks: OKFChunk[] = []; let chunkIndex = 0; + // Base Primary Metadata Chunk (Guarantees 100% indexing for ALL assets including URLs & Documents) + chunks.push({ + chunkIndex: chunkIndex++, + chunkType: 'TEXT', + content: `Asset Title: "${asset.title}". Type: "${asset.type}". Description: "${asset.description || ''}". Problem: "${asset.problemStatement || ''}". Solution: "${asset.solution || ''}".`, + sourceMetadata: { + assetId: asset.id, + assetTitle: asset.title, + assetType: asset.type, + location: 'Catalog Overview & Metadata', + }, + }); + // 1. Ingest Problem Statement & Solution metadata if present if (asset.problemStatement) { chunks.push({ @@ -159,22 +173,33 @@ export class ExtractionService { // A. PDF Files if (ext === '.pdf' || asset.type.includes('pdf')) { try { - const pdfData = await pdfParse(buffer); - // Split by pages if possible or chunk content - const subChunks = this.splitText(pdfData.text, 600); - subChunks.forEach((text, i) => { - chunks.push({ - chunkIndex: chunkIndex++, - chunkType: 'PAGE', - content: text, - sourceMetadata: { - assetId: asset.id, - assetTitle: asset.title, - assetType: asset.type, - location: `PDF Document: Page ${i + 1}`, - }, + let pdfText = ''; + try { + const parser = new pdfParse({ data: buffer }); + const res = await parser.getText(); + pdfText = typeof res === 'string' ? res : res?.text || ''; + } catch (e1) { + try { + const res = await pdfParse(buffer); + pdfText = typeof res === 'string' ? res : res?.text || ''; + } catch (e2) {} + } + if (pdfText) { + const subChunks = this.splitText(pdfText, 600); + subChunks.forEach((text, i) => { + chunks.push({ + chunkIndex: chunkIndex++, + chunkType: 'PAGE', + content: text, + sourceMetadata: { + assetId: asset.id, + assetTitle: asset.title, + assetType: asset.type, + location: `PDF Document: Page ${i + 1}`, + }, + }); }); - }); + } } catch (e) { console.error('PDF extraction error:', e); } } // B. Word Files (.docx, .doc) diff --git a/Channel-Backend/src/services/mail.service.ts b/Channel-Backend/src/services/mail.service.ts index 45bfbf3..5b86216 100644 --- a/Channel-Backend/src/services/mail.service.ts +++ b/Channel-Backend/src/services/mail.service.ts @@ -25,6 +25,14 @@ export class MailService { const origin = originStorage.getStore() || process.env.CLIENT_ORIGIN || 'http://localhost:5173'; const inviteUrl = `${origin}/invite?token=${inviteToken}`; + const isRealEmailAllowed = process.env.ENABLE_REAL_EMAILS === 'true' && process.env.NODE_ENV === 'production'; + + if (!isRealEmailAllowed) { + console.log(`[DEV EMAIL SAFEGUARD] Blocked real email dispatch to real user/client: ${email}`); + console.log(`[DEV EMAIL SAFEGUARD] Mock Invite URL: ${inviteUrl}`); + return { messageId: 'mock-dev-safeguard-id' }; + } + const mailOptions = { from: process.env.SMTP_FROM || (process.env.SMTP_USER ? `"Tech4Biz Portal" <${process.env.SMTP_USER}>` : '"Tech4Biz Portal" '), to: email, @@ -69,6 +77,13 @@ export class MailService { public async sendCustomAnnouncement(options: { recipients: string[]; subject: string; messageBody: string }) { if (!options.recipients || options.recipients.length === 0) return; + + const isRealEmailAllowed = process.env.ENABLE_REAL_EMAILS === 'true' && process.env.NODE_ENV === 'production'; + if (!isRealEmailAllowed) { + console.log(`[DEV EMAIL SAFEGUARD] Blocked announcement email to ${options.recipients.length} recipients (Development mode safety guard). Subject: "${options.subject}"`); + return; + } + const mailOptions = { from: process.env.SMTP_FROM || '"Tech4Biz Portal" ', to: options.recipients.join(', '), diff --git a/Channel-Backend/src/utils/seed-taxonomy.ts b/Channel-Backend/src/utils/seed-taxonomy.ts index 13e459e..0148a71 100644 --- a/Channel-Backend/src/utils/seed-taxonomy.ts +++ b/Channel-Backend/src/utils/seed-taxonomy.ts @@ -1,108 +1,209 @@ +import dotenv from 'dotenv'; +dotenv.config(); + import prisma from './db'; -export async function seedFourGroupTaxonomy() { - console.log('[Taxonomy Seeder] Starting 4-Group Taxonomy Seeding...'); +async function seedTaxonomy() { + console.log('[Taxonomy-Seed] Starting taxonomy re-seeding with exact user specifications...'); - // 1. Group 1: Industry Verticals (AI & ML removed to prevent double-counting) + await prisma.vertical.deleteMany(); + await prisma.techStack.deleteMany(); + await prisma.engagementType.deleteMany(); + await prisma.complianceStandard.deleteMany(); + + console.log('[Taxonomy-Seed] Cleared existing taxonomy tables.'); + + // 1. Group 1: Industry Verticals (11 Entries) const verticalsData = [ - { name: 'Cybersecurity', slug: 'cybersecurity', color: '#3b82f6', description: 'OT, Infrastructure & Data Defense' }, - { name: 'Healthcare & Pharma', slug: 'healthcare-pharma', color: '#ec4899', description: 'Medical, Diagnostics & BioTech' }, - { name: 'Finance & Banking', slug: 'finance-banking', color: '#10b981', description: 'FinTech, Payments & Risk Analytics' }, - { name: 'Insurance', slug: 'insurance', color: '#6366f1', description: 'InsurTech, Claims & Underwriting' }, - { name: 'Energy & Utilities', slug: 'energy-utilities', color: '#f59e0b', description: 'Smart Grid, Power & Renewables' }, - { name: 'Agriculture', slug: 'agriculture', color: '#84cc16', description: 'AgriTech, Precision Farming & Supply' }, - { name: 'Education', slug: 'education', color: '#8b5cf6', description: 'EdTech, LMS & Digital Classrooms' }, - { name: 'Manufacturing & IoT', slug: 'manufacturing-iot', color: '#06b6d4', description: 'Industry 4.0, Robotics & Sensors' }, - { name: 'Automotive', slug: 'automotive', color: '#ef4444', description: 'EV, Autonomous Systems & Fleet' }, - { name: 'Retail & Supply Chain', slug: 'retail-supply-chain', color: '#f97316', description: 'E-Commerce, Logistics & Inventory' }, - { name: 'Blockchain', slug: 'blockchain', color: '#14b8a6', description: 'Web3, Smart Contracts & Distributed Ledgers' }, + { name: 'Cybersecurity & OT Security', slug: 'cybersecurity-ot-security', icon: 'Shield', description: 'ICS/SCADA protection, threat detection, and OT security architecture.', color: '#ef4444', orderIndex: 1 }, + { name: 'Healthcare & Pharma', slug: 'healthcare-pharma', icon: 'Heart', description: 'Patient monitoring, clinical workflows, and pharma tech.', color: '#10b981', orderIndex: 2 }, + { name: 'Finance & Banking', slug: 'finance-banking', icon: 'CreditCard', description: 'Core banking systems, fraud detection, and fintech platforms.', color: '#3b82f6', orderIndex: 3 }, + { name: 'Insurance', slug: 'insurance', icon: 'ShieldCheck', description: 'InsurTech systems, claim automation, and actuarial analytics.', color: '#0284c7', orderIndex: 4 }, + { name: 'Energy & Utilities', slug: 'energy-utilities', icon: 'Zap', description: 'Grid monitoring, renewable management, and infrastructure tech.', color: '#06b6d4', orderIndex: 5 }, + { name: 'Agriculture', slug: 'agriculture', icon: 'Sprout', description: 'AgriTech telemetry, precision farming, and supply analytics.', color: '#84cc16', orderIndex: 6 }, + { name: 'Education', slug: 'education', icon: 'GraduationCap', description: 'EdTech platforms, AI tutoring, and campus management.', color: '#f59e0b', orderIndex: 7 }, + { name: 'Manufacturing & IoT', slug: 'manufacturing-iot', icon: 'Cpu', description: 'Predictive maintenance, IIoT telemetry, and smart factory tech.', color: '#8b5cf6', orderIndex: 8 }, + { name: 'Automotive', slug: 'automotive', icon: 'Car', description: 'Connected vehicles, EV telemetry, and autonomous systems.', color: '#ec4899', orderIndex: 9 }, + { name: 'Retail & Supply Chain', slug: 'retail-supply-chain', icon: 'ShoppingBag', description: 'Smart basket automation, inventory AI, and logistics.', color: '#f97316', orderIndex: 10 }, + { name: 'Blockchain', slug: 'blockchain', icon: 'Link', description: 'Distributed ledgers, smart contracts, and Web3 security.', color: '#6366f1', orderIndex: 11 }, ]; - for (const [idx, v] of verticalsData.entries()) { - await prisma.vertical.upsert({ - where: { slug: v.slug }, - update: { name: v.name, color: v.color, description: v.description, orderIndex: idx, isActive: true }, - create: { name: v.name, slug: v.slug, color: v.color, description: v.description, orderIndex: idx, isActive: true }, - }); + const createdVerticals: Record = {}; + for (const item of verticalsData) { + const v = await prisma.vertical.create({ data: item }); + createdVerticals[item.name] = v.id; } - // Deactivate old legacy "AI & ML" entry if present in Verticals table to prevent double counting - await prisma.vertical.updateMany({ - where: { slug: 'ai-ml' }, - data: { isActive: false } - }).catch(() => {}); - - // 2. Group 2: Technology Stack + // 2. Group 2: Technology Stack (21 Entries across 4 Categories) const techStacksData = [ - // Languages & Frameworks - { name: 'Java / Spring Boot', slug: 'java-spring-boot', category: 'Languages & Frameworks', color: '#f97316' }, - { name: 'Node.js', slug: 'nodejs', category: 'Languages & Frameworks', color: '#22c55e' }, - { name: 'Python', slug: 'python', category: 'Languages & Frameworks', color: '#3b82f6' }, - { name: 'React', slug: 'react', category: 'Languages & Frameworks', color: '#06b6d4' }, - { name: 'Go', slug: 'go', category: 'Languages & Frameworks', color: '#00add8' }, - { name: '.NET', slug: 'dotnet', category: 'Languages & Frameworks', color: '#512bd4' }, + // Languages/Frameworks + { name: 'Java / Spring Boot', slug: 'java-spring-boot', category: 'Languages & Frameworks', icon: 'Code', description: 'Enterprise backend services and Spring ecosystem.', color: '#3b82f6', orderIndex: 1 }, + { name: 'Node.js', slug: 'nodejs', category: 'Languages & Frameworks', icon: 'Server', description: 'Event-driven JavaScript/TypeScript backend runtimes.', color: '#10b981', orderIndex: 2 }, + { name: 'Python', slug: 'python', category: 'Languages & Frameworks', icon: 'FileCode', description: 'Data science, AI models, and microservices.', color: '#f59e0b', orderIndex: 3 }, + { name: 'React', slug: 'react', category: 'Languages & Frameworks', icon: 'Layout', description: 'Modern web component UIs and frontend state management.', color: '#06b6d4', orderIndex: 4 }, + { name: 'Go', slug: 'golang', category: 'Languages & Frameworks', icon: 'Cpu', description: 'High-performance cloud-native microservices.', color: '#0284c7', orderIndex: 5 }, + { name: '.NET', slug: 'dotnet', category: 'Languages & Frameworks', icon: 'Layers', description: 'C# enterprise applications and Microsoft ecosystem.', color: '#6366f1', orderIndex: 6 }, - // AI & ML - { name: 'LLM / Agentic', slug: 'llm-agentic', category: 'AI & ML', color: '#8b5cf6' }, - { name: 'RAG Systems', slug: 'rag-systems', category: 'AI & ML', color: '#a855f7' }, - { name: 'Computer Vision', slug: 'computer-vision', category: 'AI & ML', color: '#d946ef' }, - { name: 'ML Pipelines', slug: 'ml-pipelines', category: 'AI & ML', color: '#ec4899' }, - { name: 'Deepfake / Detection', slug: 'deepfake-detection', category: 'AI & ML', color: '#f43f5e' }, + // AI/ML + { name: 'AI & ML', slug: 'ai-ml', category: 'AI & ML', icon: 'Sparkles', description: 'Core artificial intelligence and machine learning models.', color: '#8b5cf6', orderIndex: 7 }, + { name: 'LLM / Agentic', slug: 'llm-agentic', category: 'AI & ML', icon: 'Bot', description: 'Large language models, multi-agent frameworks, and reasoning engines.', color: '#a855f7', orderIndex: 8 }, + { name: 'RAG', slug: 'rag', category: 'AI & ML', icon: 'Database', description: 'Retrieval-Augmented Generation and vector search systems.', color: '#ec4899', orderIndex: 9 }, + { name: 'Computer Vision', slug: 'computer-vision', category: 'AI & ML', icon: 'Camera', description: 'Real-time video analytics and optical recognition.', color: '#f43f5e', orderIndex: 10 }, + { name: 'ML Pipelines', slug: 'ml-pipelines', category: 'AI & ML', icon: 'GitBranch', description: 'MLOps, model retraining, and feature stores.', color: '#d946ef', orderIndex: 11 }, + { name: 'Deepfake / Detection', slug: 'deepfake-detection', category: 'AI & ML', icon: 'Eye', description: 'Synthetic media verification and anti-spoofing.', color: '#ef4444', orderIndex: 12 }, - // Data & Backend - { name: 'PostgreSQL', slug: 'postgresql', category: 'Data & Backend', color: '#336791' }, - { name: 'Temporal', slug: 'temporal', category: 'Data & Backend', color: '#111827' }, - { name: 'Kafka', slug: 'kafka', category: 'Data & Backend', color: '#231f20' }, - { name: 'Event-driven', slug: 'event-driven', category: 'Data & Backend', color: '#eab308' }, - { name: 'Microservices', slug: 'microservices', category: 'Data & Backend', color: '#10b981' }, + // Data/Backend + { name: 'PostgreSQL', slug: 'postgresql', category: 'Data & Backend', icon: 'Database', description: 'Relational database with JSONB and vector capabilities.', color: '#3b82f6', orderIndex: 13 }, + { name: 'Temporal', slug: 'temporal', category: 'Data & Backend', icon: 'Clock', description: 'Durable workflow execution and saga orchestrations.', color: '#10b981', orderIndex: 14 }, + { name: 'Kafka', slug: 'kafka', category: 'Data & Backend', icon: 'Activity', description: 'Distributed event streaming and message pub/sub.', color: '#f59e0b', orderIndex: 15 }, + { name: 'Event-driven', slug: 'event-driven', category: 'Data & Backend', icon: 'Zap', description: 'Asynchronous event architecture and CQRS patterns.', color: '#06b6d4', orderIndex: 16 }, + { name: 'Microservices', slug: 'microservices', category: 'Data & Backend', icon: 'Grid', description: 'Decoupled service APIs and domain-driven design.', color: '#6366f1', orderIndex: 17 }, - // Cloud & Infra - { name: 'AWS', slug: 'aws', category: 'Cloud & Infra', color: '#ff9900' }, - { name: 'Sovereign / On-prem', slug: 'sovereign-onprem', category: 'Cloud & Infra', color: '#64748b' }, - { name: 'Kubernetes', slug: 'kubernetes', category: 'Cloud & Infra', color: '#326ce5' }, - { name: 'IaaS', slug: 'iaas', category: 'Cloud & Infra', color: '#0284c7' }, + // Cloud/Infra + { name: 'AWS', slug: 'aws', category: 'Cloud & Infra', icon: 'Cloud', description: 'Amazon Web Services cloud infrastructure.', color: '#f97316', orderIndex: 18 }, + { name: 'Sovereign / On-prem', slug: 'sovereign-onprem', category: 'Cloud & Infra', icon: 'Lock', description: 'Sovereign cloud hosting and air-gapped on-premise deployments.', color: '#64748b', orderIndex: 19 }, + { name: 'Kubernetes', slug: 'kubernetes', category: 'Cloud & Infra', icon: 'Box', description: 'K8s container orchestration and mesh networking.', color: '#0284c7', orderIndex: 20 }, + { name: 'IaaS', slug: 'iaas', category: 'Cloud & Infra', icon: 'Server', description: 'Infrastructure-as-a-Service and virtualized bare metal.', color: '#475569', orderIndex: 21 }, ]; - for (const [idx, t] of techStacksData.entries()) { - await prisma.techStack.upsert({ - where: { slug: t.slug }, - update: { name: t.name, category: t.category, color: t.color, orderIndex: idx, isActive: true }, - create: { name: t.name, slug: t.slug, category: t.category, color: t.color, orderIndex: idx, isActive: true }, - }); + const createdTechStacks: Record = {}; + for (const item of techStacksData) { + const ts = await prisma.techStack.create({ data: item }); + createdTechStacks[item.name] = ts.id; } - // 3. Group 3: Engagement Type - const engagementData = [ - { name: 'Build', slug: 'build', description: 'Greenfield architecture & end-to-end development', color: '#3b82f6' }, - { name: 'Rescue', slug: 'rescue', description: 'Turnaround, refactoring & distressed project recovery', color: '#ef4444' }, - { name: 'Scale', slug: 'scale', description: 'Performance tuning, cloud expansion & enterprise scaling', color: '#10b981' }, - { name: 'Due Diligence', slug: 'due-diligence', description: 'Tech audit, risk assessment & code quality review', color: '#f59e0b' }, + // 3. Group 3: Engagement Type (4 Entries) + const engagementTypesData = [ + { name: 'Build', slug: 'build', icon: 'Wrench', description: 'Greenfield product engineering and 0-to-1 development.', color: '#3b82f6', orderIndex: 1 }, + { name: 'Rescue', slug: 'rescue', icon: 'LifeBuoy', description: 'Turnaround engineering, legacy modernization, and critical fixes.', color: '#ef4444', orderIndex: 2 }, + { name: 'Scale', slug: 'scale', icon: 'TrendingUp', description: 'Performance optimization, architecture scaling, and throughput expansion.', color: '#10b981', orderIndex: 3 }, + { name: 'Due Diligence', slug: 'due-diligence', icon: 'FileSearch', description: 'Technical audits, code reviews, and M&A architecture assessments.', color: '#f59e0b', orderIndex: 4 }, ]; - for (const [idx, e] of engagementData.entries()) { - await prisma.engagementType.upsert({ - where: { slug: e.slug }, - update: { name: e.name, description: e.description, color: e.color, orderIndex: idx, isActive: true }, - create: { name: e.name, slug: e.slug, description: e.description, color: e.color, orderIndex: idx, isActive: true }, - }); + const createdEngagementTypes: Record = {}; + for (const item of engagementTypesData) { + const et = await prisma.engagementType.create({ data: item }); + createdEngagementTypes[item.name] = et.id; } - // 4. Group 4: Compliance & Regulatory - const complianceData = [ - { name: 'HIPAA', slug: 'hipaa', description: 'Health Insurance Portability and Accountability Act', color: '#ec4899' }, - { name: 'GxP', slug: 'gxp', description: 'Good Practice regulations (FDA/EMEA Pharma)', color: '#8b5cf6' }, - { name: 'APRA CPS 230', slug: 'apra-cps-230', description: 'Operational Risk Management (Australian Prudential)', color: '#3b82f6' }, - { name: 'SOC 2', slug: 'soc-2', description: 'Security, Availability, and Confidentiality Trust Criteria', color: '#10b981' }, - { name: 'GDPR / Sovereign', slug: 'gdpr-sovereign', description: 'EU General Data Protection & Data Sovereignty', color: '#06b6d4' }, + // 4. Group 4: Compliance / Regulatory (5 Entries) + const complianceStandardsData = [ + { name: 'HIPAA', slug: 'hipaa', icon: 'Activity', description: 'Health Insurance Portability and Accountability Act.', color: '#ec4899', orderIndex: 1 }, + { name: 'GxP', slug: 'gxp', icon: 'ShieldCheck', description: 'Good Practice quality guidelines for pharma and life sciences.', color: '#10b981', orderIndex: 2 }, + { name: 'APRA CPS 230', slug: 'apra-cps-230', icon: 'Building', description: 'APRA Operational Risk Management standard for banking.', color: '#3b82f6', orderIndex: 3 }, + { name: 'SOC 2', slug: 'soc-2', icon: 'FileCheck', description: 'SOC 2 security, availability, and confidentiality controls.', color: '#06b6d4', orderIndex: 4 }, + { name: 'GDPR / Sovereign', slug: 'gdpr-sovereign', icon: 'Lock', description: 'EU General Data Protection Regulation and data sovereignty.', color: '#8b5cf6', orderIndex: 5 }, ]; - for (const [idx, c] of complianceData.entries()) { - await prisma.complianceStandard.upsert({ - where: { slug: c.slug }, - update: { name: c.name, description: c.description, color: c.color, orderIndex: idx, isActive: true }, - create: { name: c.name, slug: c.slug, description: c.description, color: c.color, orderIndex: idx, isActive: true }, - }); + const createdCompliance: Record = {}; + for (const item of complianceStandardsData) { + const cs = await prisma.complianceStandard.create({ data: item }); + createdCompliance[item.name] = cs.id; } - console.log('[Taxonomy Seeder] 4-Group Taxonomy Seeding completed successfully.'); + console.log('[Taxonomy-Seed] Successfully seeded 11 Verticals, 21 Tech Stacks, 4 Engagement Types, and 5 Compliance Standards.'); + + // 5. Re-map Catalog Assets to the Exact Taxonomy Entries + const assets = await prisma.asset.findMany(); + console.log(`[Taxonomy-Seed] Mapping exact taxonomy relations for ${assets.length} catalog assets...`); + + let updatedCount = 0; + + for (const asset of assets) { + const text = (asset.title + ' ' + (asset.description || '') + ' ' + (asset.tags || []).join(' ')).toLowerCase(); + + const targetVerticals: string[] = []; + const targetTechs: string[] = []; + const targetEngagements: string[] = []; + const targetCompliance: string[] = []; + + // Verticals mapping + if (text.includes('cyber') || text.includes('security') || text.includes('scada') || text.includes('ot security')) { + if (createdVerticals['Cybersecurity & OT Security']) targetVerticals.push(createdVerticals['Cybersecurity & OT Security']); + } + if (text.includes('health') || text.includes('patient') || text.includes('medical') || text.includes('pharma') || text.includes('diabetic')) { + if (createdVerticals['Healthcare & Pharma']) targetVerticals.push(createdVerticals['Healthcare & Pharma']); + } + if (text.includes('bank') || text.includes('finance') || text.includes('fintech') || text.includes('payment')) { + if (createdVerticals['Finance & Banking']) targetVerticals.push(createdVerticals['Finance & Banking']); + } + if (text.includes('insurance') || text.includes('claim')) { + if (createdVerticals['Insurance']) targetVerticals.push(createdVerticals['Insurance']); + } + if (text.includes('energy') || text.includes('grid') || text.includes('metering') || text.includes('utility') || text.includes('water')) { + if (createdVerticals['Energy & Utilities']) targetVerticals.push(createdVerticals['Energy & Utilities']); + } + if (text.includes('agri') || text.includes('farm') || text.includes('crop')) { + if (createdVerticals['Agriculture']) targetVerticals.push(createdVerticals['Agriculture']); + } + if (text.includes('student') || text.includes('education') || text.includes('textbook') || text.includes('school') || text.includes('plagiarism')) { + if (createdVerticals['Education']) targetVerticals.push(createdVerticals['Education']); + } + if (text.includes('manufactur') || text.includes('iot') || text.includes('sensor') || text.includes('factory')) { + if (createdVerticals['Manufacturing & IoT']) targetVerticals.push(createdVerticals['Manufacturing & IoT']); + } + if (text.includes('auto') || text.includes('vehicle') || text.includes('car') || text.includes('ev ')) { + if (createdVerticals['Automotive']) targetVerticals.push(createdVerticals['Automotive']); + } + if (text.includes('retail') || text.includes('basket') || text.includes('store') || text.includes('supply')) { + if (createdVerticals['Retail & Supply Chain']) targetVerticals.push(createdVerticals['Retail & Supply Chain']); + } + if (text.includes('blockchain') || text.includes('ledger') || text.includes('web3')) { + if (createdVerticals['Blockchain']) targetVerticals.push(createdVerticals['Blockchain']); + } + + // Tech Stacks mapping + if (text.includes('ai') || text.includes('ml') || text.includes('model') || text.includes('predictive') || text.includes('chatbot')) { + if (createdTechStacks['AI & ML']) targetTechs.push(createdTechStacks['AI & ML']); + } + if (text.includes('llm') || text.includes('agent') || text.includes('gpt') || text.includes('deepseek')) { + if (createdTechStacks['LLM / Agentic']) targetTechs.push(createdTechStacks['LLM / Agentic']); + } + if (text.includes('rag') || text.includes('vector') || text.includes('retrieval')) { + if (createdTechStacks['RAG']) targetTechs.push(createdTechStacks['RAG']); + } + if (text.includes('vision') || text.includes('camera') || text.includes('image')) { + if (createdTechStacks['Computer Vision']) targetTechs.push(createdTechStacks['Computer Vision']); + } + if (text.includes('postgres') || text.includes('db') || text.includes('sql')) { + if (createdTechStacks['PostgreSQL']) targetTechs.push(createdTechStacks['PostgreSQL']); + } + if (text.includes('aws') || text.includes('cloud') || text.includes('server')) { + if (createdTechStacks['AWS']) targetTechs.push(createdTechStacks['AWS']); + } + + // Default Fallbacks + if (targetVerticals.length === 0 && createdVerticals['Cybersecurity & OT Security']) { + targetVerticals.push(createdVerticals['Cybersecurity & OT Security']); + } + if (targetTechs.length === 0 && createdTechStacks['AI & ML']) { + targetTechs.push(createdTechStacks['AI & ML']); + } + if (createdEngagementTypes['Build']) { + targetEngagements.push(createdEngagementTypes['Build']); + } + if (createdCompliance['SOC 2']) { + targetCompliance.push(createdCompliance['SOC 2']); + } + + await prisma.asset.update({ + where: { id: asset.id }, + data: { + verticals: { connect: targetVerticals.map(id => ({ id })) }, + techStacks: { connect: targetTechs.map(id => ({ id })) }, + engagementTypes: { connect: targetEngagements.map(id => ({ id })) }, + complianceStandards: { connect: targetCompliance.map(id => ({ id })) }, + } + }); + + updatedCount++; + } + + console.log(`[Taxonomy-Seed] Successfully mapped exact taxonomy relations for ${updatedCount} assets.`); + process.exit(0); } + +seedTaxonomy().catch(err => { + console.error('[Taxonomy-Seed] Failed:', err); + process.exit(1); +}); diff --git a/Channel-Frontend/src/components/ui/ChatDrawer.tsx b/Channel-Frontend/src/components/ui/ChatDrawer.tsx index 21a865e..64a3304 100644 --- a/Channel-Frontend/src/components/ui/ChatDrawer.tsx +++ b/Channel-Frontend/src/components/ui/ChatDrawer.tsx @@ -31,6 +31,19 @@ export interface ChatSessionItem { messages?: ChatMessage[]; } +export interface AttachedEntity { + id: string; + title: string; + type: string; + entityKind: 'ASSET' | 'SHOWCASE' | 'ECOSYSTEM' | 'LEGAL'; + url?: string; + description?: string; + problemStatement?: string; + solution?: string; + thumbnailUrl?: string; + tags?: string[]; +} + export const ChatDrawer: React.FC = () => { const navigate = useNavigate(); const location = useLocation(); @@ -47,10 +60,13 @@ export const ChatDrawer: React.FC = () => { const [pastSessions, setPastSessions] = useState([]); const [loadingHistory, setLoadingHistory] = useState(false); + const [attachedEntities, setAttachedEntities] = useState([]); + const [isDraggingOver, setIsDraggingOver] = useState(false); + const [messages, setMessages] = useState([ { sender: 'ASSISTANT', - content: 'Hello! I am your **Tech4Biz AI Advisor** powered by DeepSeek. Ask me anything about our enterprise catalog assets, compliance standards, NDA agreements, or ecosystem offerings.', + content: 'Hello! I am your **Tech4Biz AI Advisor Workbench** powered by DeepSeek. **Drag and drop any asset, case study reel, ecosystem offering, or legal agreement** here to inspect, summarize, and receive instant role-tailored explanations!', } ]); const [activePreviewAsset, setActivePreviewAsset] = useState(null); @@ -63,6 +79,60 @@ export const ChatDrawer: React.FC = () => { } }, [messages, isOpen]); + // Listen for custom attach event from card button clicks + useEffect(() => { + const handleAttachEvent = (e: Event) => { + const customEvent = e as CustomEvent; + if (customEvent.detail) { + const entity = customEvent.detail as AttachedEntity; + setIsOpen(true); + setIsMinimized(false); + setAttachedEntities(prev => { + if (prev.some(item => item.id === entity.id)) return prev; + return [...prev, entity]; + }); + } + }; + + window.addEventListener('attach-ai-entity', handleAttachEvent); + return () => window.removeEventListener('attach-ai-entity', handleAttachEvent); + }, []); + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDraggingOver(true); + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDraggingOver(false); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDraggingOver(false); + + const jsonStr = e.dataTransfer.getData('application/json'); + if (jsonStr) { + try { + const entity = JSON.parse(jsonStr) as AttachedEntity; + if (entity.id && entity.title) { + setIsOpen(true); + setIsMinimized(false); + setAttachedEntities(prev => { + if (prev.some(item => item.id === entity.id)) return prev; + return [...prev, entity]; + }); + } + } catch (err) { + console.error('Failed to parse dropped entity payload', err); + } + } + }; + // Load user session history when history drawer is opened const fetchSessionHistory = async () => { setLoadingHistory(true); @@ -91,10 +161,11 @@ export const ChatDrawer: React.FC = () => { setMessages([ { sender: 'ASSISTANT', - content: 'Started a new session! I am your **Tech4Biz AI Advisor**. Ask me anything about your catalog documents, NDA agreements, or platform features.', + content: 'Started a new session! **Drag & Drop** any catalog asset, case study, or ecosystem offering below for instant AI analysis.', } ]); setShowHistory(false); + setAttachedEntities([]); } catch (err) { console.error('Failed to create new session', err); } finally { @@ -122,7 +193,7 @@ export const ChatDrawer: React.FC = () => { ]); setShowHistory(false); } catch (err) { - console.error('Failed to load session messages', err); + console.error('Failed to load session', err); } finally { setLoading(false); } @@ -130,22 +201,27 @@ export const ChatDrawer: React.FC = () => { const handleSend = async (customPrompt?: string) => { const textToSend = customPrompt || prompt; - if (!textToSend.trim() || loading) return; + if ((!textToSend.trim() && attachedEntities.length === 0) || loading) return; + + const effectiveText = textToSend.trim() || `Explain and analyze the attached ${attachedEntities.length} dropped entity/entities in detail.`; const userMsg: ChatMessage = { sender: 'USER', - content: textToSend, + content: effectiveText + (attachedEntities.length > 0 ? `\n\nšŸ“Œ *Attached Items:* ${attachedEntities.map(e => e.title).join(', ')}` : ''), createdAt: new Date().toISOString(), }; setMessages(prev => [...prev, userMsg]); if (!customPrompt) setPrompt(''); + const currentAttached = [...attachedEntities]; + setAttachedEntities([]); setLoading(true); try { const response = await axiosInstance.post('/chat/query', { - prompt: textToSend, + prompt: effectiveText, sessionId, + attachedEntities: currentAttached, }); const { sessionId: newSessionId, message } = response.data; @@ -192,7 +268,7 @@ export const ChatDrawer: React.FC = () => { } const res = await axiosInstance.get(`/assets/${cite.assetId}`); - setActivePreviewAsset(res.data.asset); + setActivePreviewAsset(res.data); } catch (err) { console.error('Failed to fetch asset from API, constructing fallback preview:', err); let videoUrl = ''; @@ -241,25 +317,29 @@ export const ChatDrawer: React.FC = () => { }; const getDimensions = () => { - if (isMinimized) return { width: '380px', height: '56px' }; - if (drawerSize === 'maximized') return { width: '860px', height: '82vh' }; - if (drawerSize === 'wide') return { width: '640px', height: '680px' }; - return { width: '440px', height: '580px' }; + if (isMinimized) return { width: 'min(380px, 94vw)', height: '56px' }; + if (drawerSize === 'maximized') return { width: 'min(880px, 96vw)', height: 'min(84vh, 900px)' }; + if (drawerSize === 'wide') return { width: 'min(660px, 95vw)', height: 'min(700px, 82vh)' }; + return { width: 'min(440px, 95vw)', height: 'min(600px, 80vh)' }; }; const dimensions = getDimensions(); return ( <> - {/* Floating Trigger Pill */} + {/* Draggable Floating Trigger Pill */} {!isOpen && ( { setIsOpen(true); setIsMinimized(false); }} - className="fixed bottom-6 right-6 z-50 flex items-center gap-2.5 px-4 py-3 bg-slate-900 text-white rounded-full shadow-2xl border border-slate-700/80 hover:border-emerald-500/80 transition-all cursor-pointer group" + className="fixed bottom-6 right-6 z-50 flex items-center gap-2.5 px-4 py-3 bg-slate-900 text-white rounded-full shadow-2xl border border-slate-700/80 hover:border-emerald-500/80 transition-all cursor-grab active:cursor-grabbing group select-none" >
@@ -278,6 +358,9 @@ export const ChatDrawer: React.FC = () => { {isOpen && ( { }} exit={{ opacity: 0, y: 40, scale: 0.95 }} transition={{ type: 'spring', damping: 25, stiffness: 220 }} - className="fixed bottom-6 right-6 z-50 bg-slate-950/95 backdrop-blur-2xl border border-slate-800 rounded-3xl shadow-2xl flex flex-col overflow-hidden text-slate-100 font-sans" + className="fixed sm:bottom-6 sm:right-6 bottom-0 right-0 left-0 sm:left-auto z-50 bg-slate-950/95 backdrop-blur-2xl border border-slate-800 sm:rounded-3xl rounded-t-2xl shadow-2xl flex flex-col overflow-hidden text-slate-100 font-sans max-w-full" > {/* Window Header (Clean Layout) */}
@@ -507,27 +590,41 @@ export const ChatDrawer: React.FC = () => {
- {/* Quick Suggestion Chips */} -
- - - -
+ {/* Attached Entities Chip Bar */} + {attachedEntities.length > 0 && ( +
+
+ + + Attached Workbench Entities ({attachedEntities.length}): + + +
+ {attachedEntities.map(ent => ( + + + {ent.entityKind} + + {ent.title} + + + ))} +
+ )} {/* Input Controls */}
@@ -536,18 +633,31 @@ export const ChatDrawer: React.FC = () => { value={prompt} onChange={(e) => setPrompt(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSend()} - placeholder="Ask about assets, NDA agreements, or portal features..." + placeholder={attachedEntities.length > 0 ? "Ask AI to analyze or summarize attached items..." : "Ask AI or drag and drop items here..."} disabled={loading} className="flex-1 bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-emerald-500/60 transition-all font-sans" />
+ + {/* Drag and Drop Target Zone Overlay */} + {isDraggingOver && ( +
+ +

+ Drop Entity Here for Instant AI Workbench Inspection +

+

+ Attach catalog assets, case studies, partner offerings, or legal agreements to analyze and summarize. +

+
+ )} )} diff --git a/Channel-Frontend/src/components/ui/MarkdownViewer.tsx b/Channel-Frontend/src/components/ui/MarkdownViewer.tsx index 5c57587..6bf8fa0 100644 --- a/Channel-Frontend/src/components/ui/MarkdownViewer.tsx +++ b/Channel-Frontend/src/components/ui/MarkdownViewer.tsx @@ -1,16 +1,19 @@ import React from "react"; interface MarkdownBlock { - type: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "code" | "blockquote" | "ul" | "ol" | "hr" | "p"; + type: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "code" | "blockquote" | "ul" | "ol" | "hr" | "p" | "table"; content: string; language?: string; items?: string[]; + headers?: string[]; + rows?: string[][]; } interface ParseState { blocks: MarkdownBlock[]; currentCodeBlock: { language: string; lines: string[] } | null; currentList: { type: "ul" | "ol"; items: string[] } | null; + currentTableLines: string[]; currentParagraphLines: string[]; } @@ -41,6 +44,36 @@ const flushList = (state: ParseState): void => { } }; +const flushTable = (state: ParseState): void => { + if (state.currentTableLines.length > 0) { + const rawLines = state.currentTableLines; + state.currentTableLines = []; + + // Filter out separator lines like |---|---| + const parsedRows = rawLines + .filter(line => !/^[|\s-:]+$/.test(line.trim())) + .map(line => { + const cells = line.split('|').map(c => c.trim()); + // Remove empty lead/trail cells from leading/trailing pipes + if (cells.length > 0 && cells[0] === '') cells.shift(); + if (cells.length > 0 && cells[cells.length - 1] === '') cells.pop(); + return cells; + }) + .filter(row => row.length > 0); + + if (parsedRows.length > 0) { + const headers = parsedRows[0]; + const rows = parsedRows.slice(1); + state.blocks.push({ + type: "table", + content: "", + headers, + rows, + }); + } + } +}; + const handleCodeBlock = (trimmed: string, state: ParseState): boolean => { if (trimmed.startsWith("```")) { if (state.currentCodeBlock) { @@ -53,6 +86,7 @@ const handleCodeBlock = (trimmed: string, state: ParseState): boolean => { } else { flushParagraph(state); flushList(state); + flushTable(state); const language = trimmed.slice(3).trim(); state.currentCodeBlock = { language, lines: [] }; } @@ -66,6 +100,7 @@ const handleHeading = (line: string, state: ParseState): boolean => { if (match) { flushParagraph(state); flushList(state); + flushTable(state); const level = match[1].length; state.blocks.push({ type: `h${level}` as any, @@ -80,6 +115,7 @@ const handleBlockquote = (trimmed: string, state: ParseState): boolean => { if (trimmed.startsWith(">")) { flushParagraph(state); flushList(state); + flushTable(state); state.blocks.push({ type: "blockquote", content: trimmed.replace(/^>\s*/, ""), @@ -93,6 +129,7 @@ const handleLists = (line: string, state: ParseState): boolean => { const ulMatch = line.match(/^(\s*)([-*+])\s+(.*)$/); if (ulMatch) { flushParagraph(state); + flushTable(state); const content = ulMatch[3].trim(); if (state.currentList && state.currentList.type === "ul") { state.currentList.items.push(content); @@ -106,6 +143,7 @@ const handleLists = (line: string, state: ParseState): boolean => { const olMatch = line.match(/^(\s*)(\d+)\.\s+(.*)$/); if (olMatch) { flushParagraph(state); + flushTable(state); const content = olMatch[3].trim(); if (state.currentList && state.currentList.type === "ol") { state.currentList.items.push(content); @@ -119,6 +157,17 @@ const handleLists = (line: string, state: ParseState): boolean => { return false; }; +const handleTableLine = (trimmed: string, state: ParseState): boolean => { + // Check if line contains markdown table pipes + if (trimmed.includes("|") && (trimmed.startsWith("|") || trimmed.includes(" | ") || /^[-|\s:]+$/.test(trimmed))) { + flushParagraph(state); + flushList(state); + state.currentTableLines.push(trimmed); + return true; + } + return false; +}; + const handleLine = (line: string, state: ParseState): void => { const trimmed = line.trim(); @@ -134,6 +183,7 @@ const handleLine = (line: string, state: ParseState): void => { if (trimmed === "---" || trimmed === "***" || trimmed === "___") { flushParagraph(state); flushList(state); + flushTable(state); state.blocks.push({ type: "hr", content: "" }); return; } @@ -146,22 +196,48 @@ const handleLine = (line: string, state: ParseState): void => { return; } + if (handleTableLine(trimmed, state)) { + return; + } + if (trimmed === "") { flushParagraph(state); flushList(state); + flushTable(state); return; } flushList(state); + flushTable(state); state.currentParagraphLines.push(line); }; +/** + * Preprocesses raw markdown text to split single-line concatenated markdown table rows into proper multi-line Markdown tables. + */ +const sanitizeMarkdownText = (rawText: string): string => { + if (!rawText) return ""; + + let formatted = rawText; + + // Split inline concatenated table rows like "| Col 1 | Col 2 | | :--- | :--- | | Val 1 | Val 2 |" + formatted = formatted.replace(/\|\s*\|\s*:-/g, "|\n| :-"); + formatted = formatted.replace(/\|\s*\|\s*([A-Za-z0-9_*`])/g, "|\n| $1"); + formatted = formatted.replace(/([^\n|])\s*(\|[\s\S]+?\|)\s*([^\n|])/g, "$1\n\n$2\n\n$3"); + + // Clean up repeated linebreaks + formatted = formatted.replace(/\n{3,}/g, "\n\n"); + return formatted; +}; + export const parseMarkdown = (text: string): MarkdownBlock[] => { - const lines = text.split("\n"); + const sanitized = sanitizeMarkdownText(text); + const lines = sanitized.split("\n"); const state: ParseState = { blocks: [], currentCodeBlock: null, currentList: null, + currentTableLines: [], currentParagraphLines: [], }; @@ -171,6 +247,7 @@ export const parseMarkdown = (text: string): MarkdownBlock[] => { flushParagraph(state); flushList(state); + flushTable(state); return state.blocks; }; @@ -357,12 +434,45 @@ const renderHeadingBlock = (block: MarkdownBlock, key: string, isDark: boolean): ); }; +const renderTableBlock = (block: MarkdownBlock, key: string, isDark: boolean): React.ReactNode => { + if (!block.headers || block.headers.length === 0) return null; + + return ( +
+ + + + {block.headers.map((h, i) => ( + + ))} + + + + {(block.rows || []).map((row, rIdx) => ( + + {row.map((cell, cIdx) => ( + + ))} + + ))} + +
+ {renderInlineText(h, isDark)} +
+ {renderInlineText(cell, isDark)} +
+
+ ); +}; + const renderBlock = (block: MarkdownBlock, index: number, isDark: boolean): React.ReactNode => { const key = `${block.type}-${index}`; if (block.type.startsWith("h") && block.type.length === 2 && block.type !== "hr") { return renderHeadingBlock(block, key, isDark); } switch (block.type) { + case "table": + return renderTableBlock(block, key, isDark); case "blockquote": return (
@@ -374,7 +484,7 @@ const renderBlock = (block: MarkdownBlock, index: number, isDark: boolean): Reac return renderListBlock(block, key, isDark); case "code": return ( -
+
{block.language && (
{block.language} @@ -405,7 +515,7 @@ export const MarkdownViewer: React.FC = ({ markdown, varian const blocks = parseMarkdown(markdown); const isDark = variant === 'dark'; return ( -
+
{blocks.map((block, idx) => renderBlock(block, idx, isDark))}
); diff --git a/Channel-Frontend/src/features/assets/components/AssetCard.tsx b/Channel-Frontend/src/features/assets/components/AssetCard.tsx index 4fe952a..ebd9d25 100644 --- a/Channel-Frontend/src/features/assets/components/AssetCard.tsx +++ b/Channel-Frontend/src/features/assets/components/AssetCard.tsx @@ -14,8 +14,7 @@ import { Clock, AlertCircle, Globe, - Sparkles, - Shield + Sparkles } from 'lucide-react'; import type { Asset } from '../../../types/assets'; import type { User } from '../../../types/auth'; @@ -143,10 +142,44 @@ export const AssetCard: React.FC = ({ const hasBanner = isImage || !!asset.thumbnailUrl; const bannerSrc = asset.thumbnailUrl ? asset.thumbnailUrl : asset.url; + const handleInspectWithAI = (e: React.MouseEvent) => { + e.stopPropagation(); + const payload = { + id: asset.id, + title: asset.title, + type: asset.type, + entityKind: 'ASSET', + url: asset.url, + description: asset.description, + problemStatement: asset.problemStatement, + solution: asset.solution, + thumbnailUrl: asset.thumbnailUrl, + tags: asset.tags, + }; + window.dispatchEvent(new CustomEvent('attach-ai-entity', { detail: payload })); + }; + return ( { + const payload = { + id: asset.id, + title: asset.title, + type: asset.type, + entityKind: 'ASSET', + url: asset.url, + description: asset.description, + problemStatement: asset.problemStatement, + solution: asset.solution, + thumbnailUrl: asset.thumbnailUrl, + tags: asset.tags, + }; + e.dataTransfer.setData('application/json', JSON.stringify(payload)); + e.dataTransfer.setData('text/plain', asset.title); + }} transition={{ type: "spring", stiffness: 320, damping: 28 }} onClick={(e) => { const target = e.target as HTMLElement; @@ -188,10 +221,19 @@ export const AssetCard: React.FC = ({
+ + {isRenderable(asset.type, asset.url) && ( @@ -209,7 +251,17 @@ export const AssetCard: React.FC = ({ {isMenuOpen && ( <>
setActiveMenuId(null)} /> -
+
+
-
- {/* Group 1: Industry Verticals */} - {asset.verticals && asset.verticals.map(v => ( - - - {v.name} - - ))} - - {/* Group 2: Tech Stack */} - {asset.techStacks && asset.techStacks.map(t => ( - - - {t.name} - - ))} - - {/* Group 3: Engagement Type */} - {asset.engagementTypes && asset.engagementTypes.map(e => ( - - {e.name} - - ))} - - {/* Group 4: Compliance Standards */} - {asset.complianceStandards && asset.complianceStandards.map(c => ( - - - {c.name} - - ))} - -
- {asset.subcategory || asset.categoryId || 'General'} -
- {!asset.isDownloadable && ( -
+ {!asset.isDownloadable && ( +
+
Strict View Only
- )} -
+
+ )}

{asset.title} diff --git a/Channel-Frontend/src/features/assets/components/AssetDetailsModal.tsx b/Channel-Frontend/src/features/assets/components/AssetDetailsModal.tsx index 74004df..6e2e676 100644 --- a/Channel-Frontend/src/features/assets/components/AssetDetailsModal.tsx +++ b/Channel-Frontend/src/features/assets/components/AssetDetailsModal.tsx @@ -113,6 +113,88 @@ export const AssetDetailsModal: React.FC = ({

)} + {/* Taxonomy Metadata Section */} +
+

+ Taxonomy & Enterprise Classification +

+ + {/* Verticals */} + {asset.verticals && asset.verticals.length > 0 && ( +
+ Industry Verticals / Domains: +
+ {asset.verticals.map(v => ( + + + {v.name} + + ))} +
+
+ )} + + {/* Tech Stacks */} + {asset.techStacks && asset.techStacks.length > 0 && ( +
+ Tech Stack & Capabilities: +
+ {asset.techStacks.map(t => ( + + + {t.name} + + ))} +
+
+ )} + + {/* Engagement Types */} + {asset.engagementTypes && asset.engagementTypes.length > 0 && ( +
+ Engagement Type: +
+ {asset.engagementTypes.map(e => ( + + {e.name} + + ))} +
+
+ )} + + {/* Compliance Standards */} + {asset.complianceStandards && asset.complianceStandards.length > 0 && ( +
+ Compliance & Governance: +
+ {asset.complianceStandards.map(c => ( + + šŸ›”ļø {c.name} + + ))} +
+
+ )} +
+ {asset.tags.length > 0 && (

Tags

diff --git a/Channel-Frontend/src/features/assets/components/AssetTableView.tsx b/Channel-Frontend/src/features/assets/components/AssetTableView.tsx index b941384..1db47fd 100644 --- a/Channel-Frontend/src/features/assets/components/AssetTableView.tsx +++ b/Channel-Frontend/src/features/assets/components/AssetTableView.tsx @@ -53,6 +53,7 @@ export const AssetTableView: React.FC = ({ return ( onToggleSelect(asset.id, e)} className={`group hover:bg-slate-100/70 dark:hover:bg-slate-800/60 transition-colors cursor-pointer ${ diff --git a/Channel-Frontend/src/features/assets/components/AssetViewerModal.tsx b/Channel-Frontend/src/features/assets/components/AssetViewerModal.tsx index 57f7218..22e57a4 100644 --- a/Channel-Frontend/src/features/assets/components/AssetViewerModal.tsx +++ b/Channel-Frontend/src/features/assets/components/AssetViewerModal.tsx @@ -164,8 +164,8 @@ export const AssetViewerModal: React.FC = ({ ); }; - const isWord = asset ? (asset.type.includes('word') || asset.url.toLowerCase().endsWith('.docx') || asset.url.toLowerCase().endsWith('.doc')) : false; - const isSpreadsheet = asset ? (asset.type.includes('sheet') || asset.url.toLowerCase().endsWith('.xlsx') || asset.url.toLowerCase().endsWith('.xls') || asset.url.toLowerCase().endsWith('.csv')) : false; + const isWord = asset ? (((asset.type || '').includes('word') || (asset.url || '').toLowerCase().endsWith('.docx') || (asset.url || '').toLowerCase().endsWith('.doc'))) : false; + const isSpreadsheet = asset ? (((asset.type || '').includes('sheet') || (asset.url || '').toLowerCase().endsWith('.xlsx') || (asset.url || '').toLowerCase().endsWith('.xls') || (asset.url || '').toLowerCase().endsWith('.csv'))) : false; useEffect(() => { if (isOpen && asset) { diff --git a/Channel-Frontend/src/features/assets/components/ShareAssetModal.tsx b/Channel-Frontend/src/features/assets/components/ShareAssetModal.tsx index 0264288..c85e225 100644 --- a/Channel-Frontend/src/features/assets/components/ShareAssetModal.tsx +++ b/Channel-Frontend/src/features/assets/components/ShareAssetModal.tsx @@ -32,6 +32,8 @@ export const ShareAssetModal: React.FC = ({ const [groups, setGroups] = useState([]); const [selectedGroupId, setSelectedGroupId] = useState(''); + const [shareMode, setShareMode] = useState<'ALL' | 'SELECTED'>('ALL'); + useEffect(() => { if (isOpen) { getAssetGroups().then(setGroups).catch(console.error); @@ -41,16 +43,22 @@ export const ShareAssetModal: React.FC = ({ useEffect(() => { if (asset) { - setSharesList( - asset.sharedWith?.map(s => ({ - organizationId: s.organizationId, - userId: s.userId - })) || [] + const existingShares = asset.sharedWith?.map(s => ({ + organizationId: s.organizationId, + userId: s.userId + })) || []; + setSharesList(existingShares); + + // Pre-select mode: if shared with all orgs, set ALL, otherwise SELECTED + const isSharedWithAll = organizations.length > 0 && organizations.every(org => + existingShares.some(s => s.organizationId === org.id && s.userId === null) ); + setShareMode(isSharedWithAll ? 'ALL' : (existingShares.length === 0 ? 'ALL' : 'SELECTED')); } else { setSharesList([]); + setShareMode('ALL'); } - }, [asset, isOpen]); + }, [asset, isOpen, organizations]); const isOrgSharedEntirely = (orgId: string) => { return sharesList.some(s => s.organizationId === orgId && s.userId === null); @@ -90,12 +98,16 @@ export const ShareAssetModal: React.FC = ({ setIsSavingShare(true); try { + const targetShares = shareMode === 'ALL' + ? organizations.map(org => ({ organizationId: org.id, userId: null })) + : sharesList; + if (asset) { await updateAsset(asset.id, { - shares: sharesList + shares: targetShares }); } else if (assetIds && assetIds.length > 0) { - await bulkShareAssets(assetIds, sharesList); + await bulkShareAssets(assetIds, targetShares); } if (selectedGroupId) { @@ -105,7 +117,10 @@ export const ShareAssetModal: React.FC = ({ } } - success('Share permissions updated', 'The asset visibility and group membership settings have been updated.'); + success('Share permissions updated', shareMode === 'ALL' + ? 'The asset has been shared with ALL partner organizations under the "All Assets" catalog tab.' + : 'The asset visibility and group access settings have been updated.' + ); onSuccess(); onClose(); } catch (err: any) { @@ -120,7 +135,7 @@ export const ShareAssetModal: React.FC = ({ 0))} onClose={onClose} - title="Share Settings" + title="Share & Partner Access Settings" subtitle={asset ? asset.title : `${assetIds?.length || 0} selected assets`} size="md" footer={ @@ -147,9 +162,51 @@ export const ShareAssetModal: React.FC = ({ > {(asset || (assetIds && assetIds.length > 0)) && (
-

- Select organizations or expand to specify exact users that can access this asset: -

+ + {/* Share Scope Selector Pill */} +
+ +
+ + + +
+
+ + {shareMode === 'ALL' ? ( +
+ 🌐 Global Access Mode Enabled +

+ This asset will be automatically accessible to ALL registered partner organizations and visible under the "All Assets" catalog tab. +

+
+ ) : ( +
+

+ Select specific partner organizations or expand to specify exact users: +

{organizations.length === 0 ? ( @@ -229,6 +286,8 @@ export const ShareAssetModal: React.FC = ({ }) )}
+
+ )} {groups.length > 0 && (
diff --git a/Channel-Frontend/src/features/assets/components/UploadAssetModal.tsx b/Channel-Frontend/src/features/assets/components/UploadAssetModal.tsx index 5abdb18..01d789b 100644 --- a/Channel-Frontend/src/features/assets/components/UploadAssetModal.tsx +++ b/Channel-Frontend/src/features/assets/components/UploadAssetModal.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; -import { X, UploadCloud, Eye, FileText, File, Sparkles } from 'lucide-react'; -import { uploadAsset, scrapeCaseStudy, getTaxonomyMeta } from '../../../services/assets-api'; -import type { TaxonomyMeta } from '../../../types/assets'; +import { X, UploadCloud, Eye, FileText, File, Sparkles, Globe, Users } from 'lucide-react'; +import { uploadAsset, scrapeCaseStudy, getTaxonomyMeta, getOrganizations } from '../../../services/assets-api'; +import type { TaxonomyMeta, Organization } from '../../../types/assets'; import Modal from '../../../components/ui/Modal'; import Button from '../../../components/ui/Button'; import { useToast } from '../../../hooks/use-toast'; @@ -19,6 +19,9 @@ export const UploadAssetModal: React.FC = ({ }) => { const { success, error } = useToast(); const [taxonomyMeta, setTaxonomyMeta] = useState(null); + const [organizations, setOrganizations] = useState([]); + const [shareScope, setShareScope] = useState<'ALL' | 'SELECTED'>('ALL'); + const [selectedOrgIds, setSelectedOrgIds] = useState([]); const [selectedVerticalIds, setSelectedVerticalIds] = useState([]); const [selectedTechStackIds, setSelectedTechStackIds] = useState([]); @@ -47,6 +50,7 @@ export const UploadAssetModal: React.FC = ({ useEffect(() => { getTaxonomyMeta().then(setTaxonomyMeta).catch(console.error); + getOrganizations().then(setOrganizations).catch(console.error); }, []); useEffect(() => { @@ -185,6 +189,13 @@ export const UploadAssetModal: React.FC = ({ } formData.append('includeInKnowledgeBase', String(includeInKnowledgeBase)); + if (organizations.length > 0) { + const targetShares = shareScope === 'ALL' + ? organizations.map(org => ({ organizationId: org.id, userId: null })) + : selectedOrgIds.map(orgId => ({ organizationId: orgId, userId: null })); + formData.append('shares', JSON.stringify(targetShares)); + } + try { await uploadAsset(formData); setUploadFile(null); @@ -667,6 +678,68 @@ export const UploadAssetModal: React.FC = ({
+ {/* Partner Sharing & Visibility Scope */} +
+

+ Partner Visibility & Access Scope (Admin Control) +

+
+ + + +
+ + {shareScope === 'ALL' ? ( +

+ 🌐 This asset will be automatically shared with all registered partner organizations and accessible under the "All Assets" catalog tab. +

+ ) : ( +
+ Select Target Organizations: + {organizations.length === 0 ? ( +

No partner organizations registered yet.

+ ) : ( + organizations.map(org => { + const isSelected = selectedOrgIds.includes(org.id); + return ( + + ); + }) + )} +
+ )} +
+
{ ) : (
{/* NDA Card */} -
+
{ + const payload = { + id: ndaAcceptance?.document?.id || 'nda', + title: 'Mutual Non-Disclosure Agreement (NDA)', + type: 'legal', + entityKind: 'LEGAL', + description: 'Required to protect proprietary IP, silicon designs, and private data sharing.', + url: ndaAcceptance?.documentUrl, + }; + e.dataTransfer.setData('application/json', JSON.stringify(payload)); + e.dataTransfer.setData('text/plain', 'Mutual Non-Disclosure Agreement (NDA)'); + }} + className="bg-ink-0 border border-ink-200 rounded-2xl p-5 shadow-sm flex flex-col justify-between hover:border-ink-400 transition-all duration-500 cursor-pointer" + >
-
- +
+
+ +
+
{ndaAcceptance ? ( @@ -155,11 +192,48 @@ export const ClientAgreementsPage: React.FC = () => {
{/* MSA Card */} -
+
{ + const payload = { + id: msaAcceptance?.document?.id || 'msa', + title: 'Master Services Agreement (MSA)', + type: 'legal', + entityKind: 'LEGAL', + description: 'Defines commercial framework, SLA guidelines, and consulting provisions.', + url: msaAcceptance?.documentUrl, + }; + e.dataTransfer.setData('application/json', JSON.stringify(payload)); + e.dataTransfer.setData('text/plain', 'Master Services Agreement (MSA)'); + }} + className="bg-ink-0 border border-ink-200 rounded-2xl p-5 shadow-sm flex flex-col justify-between hover:border-ink-400 transition-all duration-500 cursor-pointer" + >
-
- +
+
+ +
+
{msaAcceptance ? ( diff --git a/Channel-Frontend/src/pages/EcosystemPage.tsx b/Channel-Frontend/src/pages/EcosystemPage.tsx index a5a87af..0ad2054 100644 --- a/Channel-Frontend/src/pages/EcosystemPage.tsx +++ b/Channel-Frontend/src/pages/EcosystemPage.tsx @@ -8,7 +8,8 @@ import { Cloud, Globe, ExternalLink, - Layers + Layers, + Sparkles } from 'lucide-react'; import { PageHeader } from '../components/ui/PageHeader'; import { PageLayout } from '../components/layout/PageLayout'; @@ -127,12 +128,26 @@ export const EcosystemPage: React.FC = () => {
{ + const payload = { + id: offering.id, + title: offering.name, + type: offering.type, + entityKind: 'ECOSYSTEM', + url: offering.websiteUrl, + description: offering.description, + tagline: offering.tagline, + }; + e.dataTransfer.setData('application/json', JSON.stringify(payload)); + e.dataTransfer.setData('text/plain', offering.name); + }} + className="group flex flex-col justify-between bg-ink-0 border border-ink-200 hover:border-ink-450 rounded-3xl p-6 md:p-8 hover:shadow-xl transition-all duration-500 cursor-pointer" >
{/* Logo and Badges */}
-
+
{offering.logoUrl ? ( ) : ( @@ -142,14 +157,37 @@ export const EcosystemPage: React.FC = () => { )}
- - - {offering.type} - +
+ + + + + {offering.type} + +
{/* Info Copy */} diff --git a/Channel-Frontend/src/pages/ShowcasePage.tsx b/Channel-Frontend/src/pages/ShowcasePage.tsx index f39660d..9581208 100644 --- a/Channel-Frontend/src/pages/ShowcasePage.tsx +++ b/Channel-Frontend/src/pages/ShowcasePage.tsx @@ -8,7 +8,8 @@ import { Video, Maximize2, Tv, - Monitor + Monitor, + Sparkles } from 'lucide-react'; import { PageHeader } from '../components/ui/PageHeader'; import { PageLayout } from '../components/layout/PageLayout'; @@ -241,6 +242,20 @@ export const ShowcasePage: React.FC = () => {
{ + const payload = { + id: item.id, + title: item.title, + type: 'case_study', + entityKind: 'SHOWCASE', + url: item.youtubeUrl, + description: item.description, + thumbnailUrl: item.thumbnailUrl, + }; + e.dataTransfer.setData('application/json', JSON.stringify(payload)); + e.dataTransfer.setData('text/plain', item.title); + }} transition={{ type: "spring", stiffness: 320, damping: 28 }} className={`group flex flex-col bg-ink-0 border rounded-2xl overflow-hidden transition-[border-color,box-shadow,background-color] duration-300 ${ isExpanded @@ -250,6 +265,26 @@ export const ShowcasePage: React.FC = () => { > {/* Video Player / Thumbnail */}
+ {thumbnail ? (