Chatbot_phase1_2307
This commit is contained in:
parent
67acafd245
commit
4256143939
1
.gitignore
vendored
1
.gitignore
vendored
@ -38,3 +38,4 @@ uploads/*
|
||||
/Guide.md
|
||||
# Dedicated Documentation Folder
|
||||
/documents/
|
||||
/minio-seed/
|
||||
29
Channel-Backend/src/auto-index.ts
Normal file
29
Channel-Backend/src/auto-index.ts
Normal file
@ -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);
|
||||
});
|
||||
@ -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);
|
||||
|
||||
209
Channel-Backend/src/seed-taxonomy.ts
Normal file
209
Channel-Backend/src/seed-taxonomy.ts
Normal file
@ -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<string, string> = {};
|
||||
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<string, string> = {};
|
||||
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<string, string> = {};
|
||||
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<string, string> = {};
|
||||
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);
|
||||
});
|
||||
@ -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<string, CitationItem>();
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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" <noreply@tech4biz.com>'),
|
||||
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" <noreply@tech4biz.com>',
|
||||
to: options.recipients.join(', '),
|
||||
|
||||
@ -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<string, string> = {};
|
||||
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<string, string> = {};
|
||||
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<string, string> = {};
|
||||
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<string, string> = {};
|
||||
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);
|
||||
});
|
||||
|
||||
@ -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<ChatSessionItem[]>([]);
|
||||
const [loadingHistory, setLoadingHistory] = useState(false);
|
||||
|
||||
const [attachedEntities, setAttachedEntities] = useState<AttachedEntity[]>([]);
|
||||
const [isDraggingOver, setIsDraggingOver] = useState(false);
|
||||
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([
|
||||
{
|
||||
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<Asset | null>(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 && (
|
||||
<motion.button
|
||||
drag
|
||||
dragConstraints={{ left: -1200, right: 20, top: -800, bottom: 20 }}
|
||||
dragElastic={0.1}
|
||||
dragMomentum={false}
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => { 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"
|
||||
>
|
||||
<div className="p-1.5 rounded-full bg-emerald-500/20 text-emerald-400 border border-emerald-500/40 group-hover:scale-110 transition-transform">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
@ -278,6 +358,9 @@ export const ChatDrawer: React.FC = () => {
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
initial={{ opacity: 0, y: 40, scale: 0.95 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
@ -288,7 +371,7 @@ export const ChatDrawer: React.FC = () => {
|
||||
}}
|
||||
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) */}
|
||||
<div className="px-4 py-3 bg-slate-900/90 border-b border-slate-800 flex items-center justify-between shrink-0">
|
||||
@ -507,27 +590,41 @@ export const ChatDrawer: React.FC = () => {
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Quick Suggestion Chips */}
|
||||
<div className="px-3 py-1.5 bg-slate-900/60 border-t border-slate-800 flex gap-2 overflow-x-auto shrink-0 scrollbar-none">
|
||||
<button
|
||||
onClick={() => handleSend("Is there any NDA Agreement available here?")}
|
||||
className="px-2.5 py-1 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-full text-[10px] font-semibold whitespace-nowrap transition-colors flex items-center gap-1 cursor-pointer"
|
||||
>
|
||||
📄 Check NDA Agreements
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSend("Tell me about our AI and Cybersecurity offerings")}
|
||||
className="px-2.5 py-1 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-full text-[10px] font-semibold whitespace-nowrap transition-colors flex items-center gap-1 cursor-pointer"
|
||||
>
|
||||
🔍 Cybersecurity Assets
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSend("How to request document downloads or change themes?")}
|
||||
className="px-2.5 py-1 bg-amber-500/10 hover:bg-amber-500/20 text-amber-300 border border-amber-500/30 rounded-full text-[10px] font-semibold whitespace-nowrap transition-colors flex items-center gap-1 cursor-pointer"
|
||||
>
|
||||
⭐ Portal Guide & Help
|
||||
</button>
|
||||
</div>
|
||||
{/* Attached Entities Chip Bar */}
|
||||
{attachedEntities.length > 0 && (
|
||||
<div className="px-3 py-2 bg-slate-950 border-t border-slate-800 flex flex-wrap gap-1.5 shrink-0 max-h-28 overflow-y-auto">
|
||||
<div className="w-full flex items-center justify-between text-[10px] font-extrabold uppercase tracking-wider text-amber-400">
|
||||
<span className="flex items-center gap-1">
|
||||
<Sparkles className="w-3 h-3 text-amber-400 fill-amber-400" />
|
||||
Attached Workbench Entities ({attachedEntities.length}):
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setAttachedEntities([])}
|
||||
className="text-slate-400 hover:text-slate-200 transition-colors text-[9px] cursor-pointer"
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
{attachedEntities.map(ent => (
|
||||
<span
|
||||
key={ent.id}
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-bold bg-slate-800 text-slate-100 border border-slate-700 shadow-sm"
|
||||
>
|
||||
<span className="text-[9px] font-black uppercase px-1.5 py-0.5 rounded bg-amber-500 text-slate-950">
|
||||
{ent.entityKind}
|
||||
</span>
|
||||
<span className="truncate max-w-[160px] text-white">{ent.title}</span>
|
||||
<button
|
||||
onClick={() => setAttachedEntities(prev => prev.filter(x => x.id !== ent.id))}
|
||||
className="hover:text-red-400 transition-colors text-slate-400 cursor-pointer ml-1"
|
||||
title="Remove attached item"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input Controls */}
|
||||
<div className="p-3 bg-slate-900 border-t border-slate-800 flex gap-2 items-center shrink-0">
|
||||
@ -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"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleSend()}
|
||||
disabled={loading || !prompt.trim()}
|
||||
disabled={loading || (!prompt.trim() && attachedEntities.length === 0)}
|
||||
className="p-2 bg-emerald-500 hover:bg-emerald-400 disabled:opacity-40 text-slate-950 rounded-xl transition-all font-bold cursor-pointer disabled:cursor-not-allowed"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Drag and Drop Target Zone Overlay */}
|
||||
{isDraggingOver && (
|
||||
<div className="absolute inset-0 z-50 bg-slate-950/90 backdrop-blur-md border-4 border-dashed border-amber-500 rounded-3xl flex flex-col items-center justify-center p-6 text-center animate-pulse">
|
||||
<Sparkles className="w-12 h-12 text-amber-400 fill-amber-400 mb-3 animate-bounce" />
|
||||
<h3 className="text-lg font-black text-slate-950 bg-amber-400 px-4 py-1 rounded-full shadow-lg">
|
||||
Drop Entity Here for Instant AI Workbench Inspection
|
||||
</h3>
|
||||
<p className="text-xs font-bold text-slate-300 mt-2 max-w-xs leading-relaxed">
|
||||
Attach catalog assets, case studies, partner offerings, or legal agreements to analyze and summarize.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
@ -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 (
|
||||
<div key={key} className="my-3 w-full overflow-x-auto rounded-xl border border-slate-800 bg-slate-950/90 shadow-xl max-w-full">
|
||||
<table className="w-full text-left border-collapse min-w-[320px]">
|
||||
<thead>
|
||||
<tr className="bg-slate-900/90 border-b border-slate-800 text-[11px] font-extrabold text-emerald-400 uppercase tracking-wider">
|
||||
{block.headers.map((h, i) => (
|
||||
<th key={i} className="py-2.5 px-3 border-r last:border-r-0 border-slate-800/80 font-bold whitespace-nowrap">
|
||||
{renderInlineText(h, isDark)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800/60 text-xs">
|
||||
{(block.rows || []).map((row, rIdx) => (
|
||||
<tr key={rIdx} className="hover:bg-slate-900/60 transition-colors odd:bg-slate-950/40 even:bg-slate-900/30">
|
||||
{row.map((cell, cIdx) => (
|
||||
<td key={cIdx} className="py-2.5 px-3 border-r last:border-r-0 border-slate-800/60 text-slate-200 leading-relaxed font-sans">
|
||||
{renderInlineText(cell, isDark)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<blockquote key={key} className={`border-l-4 border-emerald-500 pl-3 py-1 italic ${isDark ? 'bg-slate-900/60 text-slate-200' : 'bg-primary-50/20 text-ink-700'} my-2 rounded-r-lg`}>
|
||||
@ -374,7 +484,7 @@ const renderBlock = (block: MarkdownBlock, index: number, isDark: boolean): Reac
|
||||
return renderListBlock(block, key, isDark);
|
||||
case "code":
|
||||
return (
|
||||
<div key={key} className="my-3 rounded-xl border border-slate-800 bg-slate-950 text-slate-100 p-3 overflow-x-auto shadow-inner relative group select-text">
|
||||
<div key={key} className="my-3 rounded-xl border border-slate-800 bg-slate-950 text-slate-100 p-3 overflow-x-auto shadow-inner relative group select-text max-w-full">
|
||||
{block.language && (
|
||||
<div className="absolute right-3 top-2 text-[9px] uppercase font-bold text-slate-400 select-none">
|
||||
{block.language}
|
||||
@ -405,7 +515,7 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = ({ markdown, varian
|
||||
const blocks = parseMarkdown(markdown);
|
||||
const isDark = variant === 'dark';
|
||||
return (
|
||||
<div className={`w-full text-left select-text font-sans leading-relaxed ${isDark ? 'text-slate-100' : 'text-ink-800'}`}>
|
||||
<div className={`w-full text-left select-text font-sans leading-relaxed break-words overflow-hidden ${isDark ? 'text-slate-100' : 'text-ink-800'}`}>
|
||||
{blocks.map((block, idx) => renderBlock(block, idx, isDark))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -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<AssetCardProps> = ({
|
||||
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 (
|
||||
<motion.div
|
||||
id={`asset-card-${asset.id}`}
|
||||
layout
|
||||
draggable={true}
|
||||
onDragStart={(e: any) => {
|
||||
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<AssetCardProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="absolute top-2 right-2 z-10 flex items-center gap-1">
|
||||
<button
|
||||
onClick={handleInspectWithAI}
|
||||
className="px-2 py-1 rounded-md bg-ink-900 text-ink-0 hover:bg-ink-800 text-[10px] font-bold shadow-sm transition-all hover:scale-105 flex items-center gap-1 border border-ink-700 cursor-pointer"
|
||||
title="Inspect with AI Advisor Workbench"
|
||||
>
|
||||
<Sparkles className="w-3 h-3 text-amber-400 fill-amber-400" />
|
||||
<span>AI Workbench</span>
|
||||
</button>
|
||||
|
||||
{isRenderable(asset.type, asset.url) && (
|
||||
<button
|
||||
onClick={() => onOpenViewer(asset)}
|
||||
className="p-1 rounded-md bg-ink-0/90 backdrop-blur text-ink-600 hover:text-ink-900 border border-ink-200 shadow-sm transition-all hover:scale-105"
|
||||
className="p-1 rounded-md bg-ink-0/90 backdrop-blur text-ink-600 hover:text-ink-900 border border-ink-200 shadow-sm transition-all hover:scale-105 cursor-pointer"
|
||||
title="Preview Online"
|
||||
>
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
@ -201,7 +243,7 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setActiveMenuId(isMenuOpen ? null : asset.id)}
|
||||
className="p-1 rounded-md bg-ink-0/90 backdrop-blur text-ink-600 hover:text-ink-900 border border-ink-200 shadow-sm transition-all hover:scale-105"
|
||||
className="p-1 rounded-md bg-ink-0/90 backdrop-blur text-ink-600 hover:text-ink-900 border border-ink-200 shadow-sm transition-all hover:scale-105 cursor-pointer"
|
||||
>
|
||||
<MoreVertical className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@ -209,7 +251,17 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
||||
{isMenuOpen && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-10" onClick={() => setActiveMenuId(null)} />
|
||||
<div className="absolute right-0 mt-1.5 w-44 bg-ink-0 border border-ink-200 rounded-lg shadow-lg z-20 overflow-hidden py-1">
|
||||
<div className="absolute right-0 mt-1.5 w-48 bg-ink-0 border border-ink-200 rounded-lg shadow-lg z-20 overflow-hidden py-1">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
handleInspectWithAI(e);
|
||||
setActiveMenuId(null);
|
||||
}}
|
||||
className="w-full text-left px-4 py-2 text-xs font-bold text-amber-650 hover:bg-amber-50 flex items-center gap-2"
|
||||
>
|
||||
<Sparkles className="w-3.5 h-3.5 text-amber-500 fill-amber-500" />
|
||||
Inspect with AI Advisor
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
onViewDetails(asset);
|
||||
@ -573,65 +625,14 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 mb-4 flex-grow flex flex-col">
|
||||
<div className="flex flex-wrap items-center gap-1.5 mb-2">
|
||||
{/* Group 1: Industry Verticals */}
|
||||
{asset.verticals && asset.verticals.map(v => (
|
||||
<span
|
||||
key={v.id}
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-bold tracking-wider border shrink-0"
|
||||
style={{
|
||||
backgroundColor: `${v.color || '#3b82f6'}15`,
|
||||
borderColor: `${v.color || '#3b82f6'}40`,
|
||||
color: v.color || '#3b82f6',
|
||||
}}
|
||||
>
|
||||
<span className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: v.color || '#3b82f6' }} />
|
||||
{v.name}
|
||||
</span>
|
||||
))}
|
||||
|
||||
{/* Group 2: Tech Stack */}
|
||||
{asset.techStacks && asset.techStacks.map(t => (
|
||||
<span
|
||||
key={t.id}
|
||||
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-semibold text-slate-700 dark:text-slate-300 bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 shrink-0"
|
||||
>
|
||||
<span className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: t.color || '#64748b' }} />
|
||||
{t.name}
|
||||
</span>
|
||||
))}
|
||||
|
||||
{/* Group 3: Engagement Type */}
|
||||
{asset.engagementTypes && asset.engagementTypes.map(e => (
|
||||
<span
|
||||
key={e.id}
|
||||
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-bold text-sky-700 dark:text-sky-300 bg-sky-50 dark:bg-sky-950/60 border border-sky-200 dark:border-sky-800 shrink-0"
|
||||
>
|
||||
{e.name}
|
||||
</span>
|
||||
))}
|
||||
|
||||
{/* Group 4: Compliance Standards */}
|
||||
{asset.complianceStandards && asset.complianceStandards.map(c => (
|
||||
<span
|
||||
key={c.id}
|
||||
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-bold text-emerald-700 dark:text-emerald-300 bg-emerald-50 dark:bg-emerald-950/60 border border-emerald-200 dark:border-emerald-800 shrink-0"
|
||||
>
|
||||
<Shield className="w-2.5 h-2.5 text-emerald-500" />
|
||||
{c.name}
|
||||
</span>
|
||||
))}
|
||||
|
||||
<div className="inline-block px-1.5 py-0.5 rounded text-[10px] font-bold text-ink-500 uppercase tracking-wider bg-ink-50 border border-ink-200">
|
||||
{asset.subcategory || asset.categoryId || 'General'}
|
||||
</div>
|
||||
{!asset.isDownloadable && (
|
||||
<div className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-bold text-ink-600 bg-ink-100 border border-ink-200">
|
||||
{!asset.isDownloadable && (
|
||||
<div className="flex items-center gap-1.5 mb-2 overflow-hidden">
|
||||
<div className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-bold text-ink-600 bg-ink-100 border border-ink-200 shrink-0">
|
||||
<Lock className="w-2.5 h-2.5" />
|
||||
<span>Strict View Only</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h3 className="font-bold text-ink-900 text-sm asset-card-title leading-snug line-clamp-2 group-hover:text-ink-900 transition-colors" title={asset.title}>
|
||||
{asset.title}
|
||||
|
||||
@ -113,6 +113,88 @@ export const AssetDetailsModal: React.FC<AssetDetailsModalProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Taxonomy Metadata Section */}
|
||||
<div className="space-y-3 pt-3 border-t border-ink-200">
|
||||
<h4 className="text-[10px] font-bold uppercase tracking-wider text-ink-500 font-sans">
|
||||
Taxonomy & Enterprise Classification
|
||||
</h4>
|
||||
|
||||
{/* Verticals */}
|
||||
{asset.verticals && asset.verticals.length > 0 && (
|
||||
<div>
|
||||
<span className="text-[10px] font-bold text-ink-600 block mb-1">Industry Verticals / Domains:</span>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{asset.verticals.map(v => (
|
||||
<span
|
||||
key={v.id}
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-bold border"
|
||||
style={{
|
||||
backgroundColor: `${v.color || '#3b82f6'}15`,
|
||||
borderColor: `${v.color || '#3b82f6'}40`,
|
||||
color: v.color || '#3b82f6',
|
||||
}}
|
||||
>
|
||||
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: v.color || '#3b82f6' }} />
|
||||
{v.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tech Stacks */}
|
||||
{asset.techStacks && asset.techStacks.length > 0 && (
|
||||
<div>
|
||||
<span className="text-[10px] font-bold text-ink-600 block mb-1">Tech Stack & Capabilities:</span>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{asset.techStacks.map(t => (
|
||||
<span
|
||||
key={t.id}
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-bold text-slate-700 dark:text-slate-200 bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700"
|
||||
>
|
||||
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: t.color || '#64748b' }} />
|
||||
{t.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Engagement Types */}
|
||||
{asset.engagementTypes && asset.engagementTypes.length > 0 && (
|
||||
<div>
|
||||
<span className="text-[10px] font-bold text-ink-600 block mb-1">Engagement Type:</span>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{asset.engagementTypes.map(e => (
|
||||
<span
|
||||
key={e.id}
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-bold text-sky-700 dark:text-sky-300 bg-sky-50 dark:bg-sky-950/60 border border-sky-200 dark:border-sky-800"
|
||||
>
|
||||
{e.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Compliance Standards */}
|
||||
{asset.complianceStandards && asset.complianceStandards.length > 0 && (
|
||||
<div>
|
||||
<span className="text-[10px] font-bold text-ink-600 block mb-1">Compliance & Governance:</span>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{asset.complianceStandards.map(c => (
|
||||
<span
|
||||
key={c.id}
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-bold text-emerald-700 dark:text-emerald-300 bg-emerald-50 dark:bg-emerald-950/60 border border-emerald-200 dark:border-emerald-800"
|
||||
>
|
||||
🛡️ {c.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{asset.tags.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-[10px] font-bold uppercase tracking-wider text-ink-500 font-sans">Tags</h4>
|
||||
|
||||
@ -53,6 +53,7 @@ export const AssetTableView: React.FC<AssetTableViewProps> = ({
|
||||
|
||||
return (
|
||||
<tr
|
||||
id={`asset-card-${asset.id}`}
|
||||
key={asset.id}
|
||||
onClick={(e) => onToggleSelect(asset.id, e)}
|
||||
className={`group hover:bg-slate-100/70 dark:hover:bg-slate-800/60 transition-colors cursor-pointer ${
|
||||
|
||||
@ -164,8 +164,8 @@ export const AssetViewerModal: React.FC<AssetViewerModalProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
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) {
|
||||
|
||||
@ -32,6 +32,8 @@ export const ShareAssetModal: React.FC<ShareAssetModalProps> = ({
|
||||
const [groups, setGroups] = useState<AssetGroup[]>([]);
|
||||
const [selectedGroupId, setSelectedGroupId] = useState<string>('');
|
||||
|
||||
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<ShareAssetModalProps> = ({
|
||||
|
||||
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<ShareAssetModalProps> = ({
|
||||
|
||||
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<ShareAssetModalProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
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<ShareAssetModalProps> = ({
|
||||
<Modal
|
||||
isOpen={isOpen && (!!asset || (!!assetIds && assetIds.length > 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<ShareAssetModalProps> = ({
|
||||
>
|
||||
{(asset || (assetIds && assetIds.length > 0)) && (
|
||||
<form id="share-asset-form" onSubmit={handleShareSubmit} className="space-y-4">
|
||||
<p className="text-xs text-ink-600 leading-relaxed font-sans">
|
||||
Select organizations or expand to specify exact users that can access this asset:
|
||||
</p>
|
||||
|
||||
{/* Share Scope Selector Pill */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] font-extrabold uppercase tracking-wider text-ink-500 block font-sans">
|
||||
Sharing Scope (Admin Access Control)
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-2 p-1 bg-ink-100 rounded-xl border border-ink-200">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShareMode('ALL')}
|
||||
className={`py-2 px-3 rounded-lg text-xs font-bold transition-all flex items-center justify-center gap-1.5 cursor-pointer font-sans ${
|
||||
shareMode === 'ALL'
|
||||
? 'bg-ink-900 text-ink-0 shadow-sm'
|
||||
: 'text-ink-600 hover:text-ink-900 hover:bg-ink-200/60'
|
||||
}`}
|
||||
>
|
||||
<span>🌐 Share with ALL Partners</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShareMode('SELECTED')}
|
||||
className={`py-2 px-3 rounded-lg text-xs font-bold transition-all flex items-center justify-center gap-1.5 cursor-pointer font-sans ${
|
||||
shareMode === 'SELECTED'
|
||||
? 'bg-ink-900 text-ink-0 shadow-sm'
|
||||
: 'text-ink-600 hover:text-ink-900 hover:bg-ink-200/60'
|
||||
}`}
|
||||
>
|
||||
<span>👥 Selected Partners Only</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{shareMode === 'ALL' ? (
|
||||
<div className="p-4 bg-emerald-500/10 border border-emerald-500/30 rounded-xl text-xs text-emerald-800 dark:text-emerald-300 font-sans space-y-1">
|
||||
<span className="font-extrabold block">🌐 Global Access Mode Enabled</span>
|
||||
<p className="leading-relaxed text-[11px]">
|
||||
This asset will be automatically accessible to <strong>ALL registered partner organizations</strong> and visible under the "All Assets" catalog tab.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-ink-600 leading-relaxed font-sans font-medium">
|
||||
Select specific partner organizations or expand to specify exact users:
|
||||
</p>
|
||||
|
||||
<div className="max-h-64 overflow-y-auto border border-ink-200 rounded-xl divide-y divide-ink-250 bg-ink-50 scrollbar-thin">
|
||||
{organizations.length === 0 ? (
|
||||
@ -229,6 +286,8 @@ export const ShareAssetModal: React.FC<ShareAssetModalProps> = ({
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{groups.length > 0 && (
|
||||
<div className="pt-2 border-t border-ink-200">
|
||||
|
||||
@ -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<UploadAssetModalProps> = ({
|
||||
}) => {
|
||||
const { success, error } = useToast();
|
||||
const [taxonomyMeta, setTaxonomyMeta] = useState<TaxonomyMeta | null>(null);
|
||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||
const [shareScope, setShareScope] = useState<'ALL' | 'SELECTED'>('ALL');
|
||||
const [selectedOrgIds, setSelectedOrgIds] = useState<string[]>([]);
|
||||
|
||||
const [selectedVerticalIds, setSelectedVerticalIds] = useState<string[]>([]);
|
||||
const [selectedTechStackIds, setSelectedTechStackIds] = useState<string[]>([]);
|
||||
@ -47,6 +50,7 @@ export const UploadAssetModal: React.FC<UploadAssetModalProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
getTaxonomyMeta().then(setTaxonomyMeta).catch(console.error);
|
||||
getOrganizations().then(setOrganizations).catch(console.error);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@ -185,6 +189,13 @@ export const UploadAssetModal: React.FC<UploadAssetModalProps> = ({
|
||||
}
|
||||
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<UploadAssetModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Partner Sharing & Visibility Scope */}
|
||||
<div className="space-y-3 pt-3 border-t border-slate-200 dark:border-slate-800">
|
||||
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">
|
||||
Partner Visibility & Access Scope (Admin Control)
|
||||
</h4>
|
||||
<div className="grid grid-cols-2 gap-2 p-1 bg-slate-100 dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShareScope('ALL')}
|
||||
className={`py-2 px-3 rounded-lg text-xs font-bold transition-all flex items-center justify-center gap-1.5 cursor-pointer font-sans ${
|
||||
shareScope === 'ALL'
|
||||
? 'bg-slate-900 text-white dark:bg-blue-600 shadow-sm'
|
||||
: 'text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700'
|
||||
}`}
|
||||
>
|
||||
<Globe className="w-3.5 h-3.5" />
|
||||
<span>Share with ALL Partners</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShareScope('SELECTED')}
|
||||
className={`py-2 px-3 rounded-lg text-xs font-bold transition-all flex items-center justify-center gap-1.5 cursor-pointer font-sans ${
|
||||
shareScope === 'SELECTED'
|
||||
? 'bg-slate-900 text-white dark:bg-blue-600 shadow-sm'
|
||||
: 'text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700'
|
||||
}`}
|
||||
>
|
||||
<Users className="w-3.5 h-3.5" />
|
||||
<span>Selected Partners Only</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{shareScope === 'ALL' ? (
|
||||
<p className="text-[11px] text-emerald-600 dark:text-emerald-400 font-medium p-2 bg-emerald-500/10 rounded-lg border border-emerald-500/20">
|
||||
🌐 This asset will be automatically shared with all registered partner organizations and accessible under the "All Assets" catalog tab.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-1.5 max-h-36 overflow-y-auto p-2 bg-slate-50 dark:bg-slate-950/60 rounded-xl border border-slate-200 dark:border-slate-800">
|
||||
<span className="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase block">Select Target Organizations:</span>
|
||||
{organizations.length === 0 ? (
|
||||
<p className="text-xs text-slate-400 italic">No partner organizations registered yet.</p>
|
||||
) : (
|
||||
organizations.map(org => {
|
||||
const isSelected = selectedOrgIds.includes(org.id);
|
||||
return (
|
||||
<label key={org.id} className="flex items-center gap-2 text-xs font-medium text-slate-800 dark:text-slate-200 cursor-pointer py-1 select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => setSelectedOrgIds(toggleSelection(selectedOrgIds, org.id))}
|
||||
className="w-3.5 h-3.5 rounded border-slate-300 dark:border-slate-700"
|
||||
/>
|
||||
<span>{org.name}</span>
|
||||
</label>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-ink-500 mb-1.5 block">Tags (comma-separated)</label>
|
||||
<input
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Shield, FileText, CheckCircle, Clock, Download, ExternalLink, RefreshCw } from 'lucide-react';
|
||||
import { Shield, FileText, CheckCircle, Clock, Download, ExternalLink, RefreshCw, Sparkles } from 'lucide-react';
|
||||
import { useAuthStore } from '../hooks/use-auth';
|
||||
import { useMyAcceptancesQuery } from '../hooks/use-legal-query';
|
||||
import PageHeader from '../components/ui/PageHeader';
|
||||
@ -80,11 +80,48 @@ export const ClientAgreementsPage: React.FC = () => {
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* NDA Card */}
|
||||
<div id={`asset-card-${ndaAcceptance?.document?.id || '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">
|
||||
<div
|
||||
id={`asset-card-${ndaAcceptance?.document?.id || 'nda'}`}
|
||||
draggable={true}
|
||||
onDragStart={(e) => {
|
||||
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"
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-ink-900 text-ink-0 flex items-center justify-center">
|
||||
<FileText className="w-5 h-5" />
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-10 h-10 rounded-xl bg-ink-900 text-ink-0 flex items-center justify-center">
|
||||
<FileText className="w-5 h-5" />
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
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,
|
||||
};
|
||||
window.dispatchEvent(new CustomEvent('attach-ai-entity', { detail: payload }));
|
||||
}}
|
||||
className="px-2.5 py-1 rounded-full bg-ink-900 text-ink-0 hover:bg-ink-950 text-[10px] font-extrabold shadow-sm transition-all hover:scale-105 flex items-center gap-1 border border-ink-700 cursor-pointer"
|
||||
title="Inspect NDA with AI Advisor Workbench"
|
||||
>
|
||||
<Sparkles className="w-3 h-3 text-amber-400 fill-amber-400" />
|
||||
<span>AI Workbench</span>
|
||||
</button>
|
||||
</div>
|
||||
{ndaAcceptance ? (
|
||||
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-bold bg-emerald-50 text-emerald-700 border border-emerald-250">
|
||||
@ -155,11 +192,48 @@ export const ClientAgreementsPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* MSA Card */}
|
||||
<div id={`asset-card-${msaAcceptance?.document?.id || '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">
|
||||
<div
|
||||
id={`asset-card-${msaAcceptance?.document?.id || 'msa'}`}
|
||||
draggable={true}
|
||||
onDragStart={(e) => {
|
||||
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"
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-ink-900 text-ink-0 flex items-center justify-center">
|
||||
<FileText className="w-5 h-5" />
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-10 h-10 rounded-xl bg-ink-900 text-ink-0 flex items-center justify-center">
|
||||
<FileText className="w-5 h-5" />
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
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,
|
||||
};
|
||||
window.dispatchEvent(new CustomEvent('attach-ai-entity', { detail: payload }));
|
||||
}}
|
||||
className="px-2.5 py-1 rounded-full bg-ink-900 text-ink-0 hover:bg-ink-950 text-[10px] font-extrabold shadow-sm transition-all hover:scale-105 flex items-center gap-1 border border-ink-700 cursor-pointer"
|
||||
title="Inspect MSA with AI Advisor Workbench"
|
||||
>
|
||||
<Sparkles className="w-3 h-3 text-amber-400 fill-amber-400" />
|
||||
<span>AI Workbench</span>
|
||||
</button>
|
||||
</div>
|
||||
{msaAcceptance ? (
|
||||
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-bold bg-emerald-50 text-emerald-700 border border-emerald-250">
|
||||
|
||||
@ -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 = () => {
|
||||
<div
|
||||
key={offering.id}
|
||||
id={`asset-card-${offering.id}`}
|
||||
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"
|
||||
draggable={true}
|
||||
onDragStart={(e) => {
|
||||
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"
|
||||
>
|
||||
<div>
|
||||
{/* Logo and Badges */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div className="h-10 flex items-center shrink-0">
|
||||
<div className="h-10 flex items-center shrink-0 gap-3">
|
||||
{offering.logoUrl ? (
|
||||
<BrandLogo name={offering.logoUrl} className="max-h-7 max-w-[150px] object-contain text-ink-900 dark:text-ink-0" />
|
||||
) : (
|
||||
@ -142,14 +157,37 @@ export const EcosystemPage: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[9px] font-black tracking-widest uppercase border ${
|
||||
isProduct
|
||||
? 'bg-blue-500/10 text-blue-600 border-blue-500/20'
|
||||
: 'bg-emerald-500/10 text-emerald-600 border-emerald-500/20'
|
||||
}`}>
|
||||
<Layers className="w-3 h-3" />
|
||||
{offering.type}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const payload = {
|
||||
id: offering.id,
|
||||
title: offering.name,
|
||||
type: offering.type,
|
||||
entityKind: 'ECOSYSTEM',
|
||||
url: offering.websiteUrl,
|
||||
description: offering.description,
|
||||
tagline: offering.tagline,
|
||||
};
|
||||
window.dispatchEvent(new CustomEvent('attach-ai-entity', { detail: payload }));
|
||||
}}
|
||||
className="px-2.5 py-1 rounded-full bg-ink-900 text-ink-0 hover:bg-ink-950 text-[10px] font-extrabold shadow-sm transition-all hover:scale-105 flex items-center gap-1 border border-ink-700 cursor-pointer"
|
||||
title="Inspect Offering with AI Advisor Workbench"
|
||||
>
|
||||
<Sparkles className="w-3 h-3 text-amber-400 fill-amber-400" />
|
||||
<span>AI Workbench</span>
|
||||
</button>
|
||||
|
||||
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[9px] font-black tracking-widest uppercase border ${
|
||||
isProduct
|
||||
? 'bg-blue-500/10 text-blue-600 border-blue-500/20'
|
||||
: 'bg-emerald-500/10 text-emerald-600 border-emerald-500/20'
|
||||
}`}>
|
||||
<Layers className="w-3 h-3" />
|
||||
{offering.type}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Copy */}
|
||||
|
||||
@ -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 = () => {
|
||||
<div key={item.id} id={`asset-card-${item.id}`} className="relative h-[410px] w-full flex flex-col">
|
||||
<motion.div
|
||||
layout
|
||||
draggable={true}
|
||||
onDragStart={(e: any) => {
|
||||
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 */}
|
||||
<div className="relative aspect-video bg-ink-900 overflow-hidden shrink-0">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const payload = {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
type: 'case_study',
|
||||
entityKind: 'SHOWCASE',
|
||||
url: item.youtubeUrl,
|
||||
description: item.description,
|
||||
thumbnailUrl: item.thumbnailUrl,
|
||||
};
|
||||
window.dispatchEvent(new CustomEvent('attach-ai-entity', { detail: payload }));
|
||||
}}
|
||||
className="absolute top-2 right-2 z-20 px-2 py-1 rounded-md bg-ink-900/90 text-ink-0 hover:bg-ink-950 text-[10px] font-bold shadow-md transition-all hover:scale-105 flex items-center gap-1 border border-ink-700 cursor-pointer"
|
||||
title="Inspect Reel with AI Advisor Workbench"
|
||||
>
|
||||
<Sparkles className="w-3 h-3 text-amber-400 fill-amber-400" />
|
||||
<span>AI Workbench</span>
|
||||
</button>
|
||||
{thumbnail ? (
|
||||
<img
|
||||
src={thumbnail}
|
||||
|
||||
@ -7,8 +7,7 @@ export default defineConfig({
|
||||
plugins: [tailwindcss(), react()],
|
||||
server: {
|
||||
allowedHosts: [
|
||||
"toughly-coinstantaneous-dimple.ngrok-free.dev",
|
||||
"spruce-fridge-destiny.ngrok-free.dev"
|
||||
"delegator-caregiver-overprice.ngrok-free.dev"
|
||||
],
|
||||
cors: true,
|
||||
proxy: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user