Chatbot_phase2_2407
This commit is contained in:
parent
4256143939
commit
4715e1b482
5
.gitignore
vendored
5
.gitignore
vendored
@ -38,4 +38,7 @@ uploads/*
|
|||||||
/Guide.md
|
/Guide.md
|
||||||
# Dedicated Documentation Folder
|
# Dedicated Documentation Folder
|
||||||
/documents/
|
/documents/
|
||||||
/minio-seed/
|
/minio-seed/
|
||||||
|
|
||||||
|
ai-advisor.md
|
||||||
|
testing-strategy.md
|
||||||
@ -149,83 +149,121 @@ export class ChatService {
|
|||||||
// Explicit Attached Entities Ingestion (Drag-and-Drop AI Workbench)
|
// Explicit Attached Entities Ingestion (Drag-and-Drop AI Workbench)
|
||||||
if (hasAttachedEntities) {
|
if (hasAttachedEntities) {
|
||||||
for (const ent of attachedEntities!) {
|
for (const ent of attachedEntities!) {
|
||||||
if (ent.entityKind === 'ASSET') {
|
if (!ent || !ent.id) continue;
|
||||||
const dbAsset = await prisma.asset.findUnique({
|
try {
|
||||||
where: { id: ent.id },
|
if (ent.entityKind === 'ASSET') {
|
||||||
include: {
|
const dbAsset = await prisma.asset.findUnique({
|
||||||
verticals: true,
|
where: { id: ent.id },
|
||||||
techStacks: true,
|
include: {
|
||||||
complianceStandards: true,
|
verticals: true,
|
||||||
|
techStacks: true,
|
||||||
|
complianceStandards: true,
|
||||||
|
}
|
||||||
|
}).catch(() => null);
|
||||||
|
|
||||||
|
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 {
|
||||||
|
citationsMap.set(ent.id, {
|
||||||
|
assetId: ent.id,
|
||||||
|
title: ent.title,
|
||||||
|
location: 'Inspected Catalog Asset',
|
||||||
|
type: ent.type || 'ASSET',
|
||||||
|
isRecommended: false,
|
||||||
|
});
|
||||||
|
contextLines.push(
|
||||||
|
`[WORKBENCH ATTACHED ASSET] Title: "${ent.title}" | Type: "${ent.type || 'Asset'}" | Description: "${ent.description || 'N/A'}" | Problem Statement: "${ent.problemStatement || 'N/A'}" | Solution Overview: "${ent.solution || 'N/A'}"\n`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
} else if (ent.entityKind === 'SHOWCASE') {
|
||||||
|
const dbShowcase = await prisma.contentShowcase.findUnique({
|
||||||
|
where: { id: ent.id }
|
||||||
|
}).catch(() => null);
|
||||||
|
|
||||||
if (dbAsset) {
|
if (dbShowcase) {
|
||||||
citationsMap.set(dbAsset.id, {
|
citationsMap.set(dbShowcase.id, {
|
||||||
assetId: dbAsset.id,
|
assetId: dbShowcase.id,
|
||||||
title: dbAsset.title,
|
title: dbShowcase.title,
|
||||||
location: 'Inspected Catalog Asset',
|
location: 'Featured Content Showcase',
|
||||||
type: dbAsset.type,
|
type: 'case_study',
|
||||||
isRecommended: false,
|
isRecommended: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
contextLines.push(
|
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`
|
`[WORKBENCH ATTACHED FEATURED REEL] Title: "${dbShowcase.title}" | Video URL: "${dbShowcase.youtubeUrl}" | Description:\n${dbShowcase.description || 'Interactive product reel'}\n`
|
||||||
);
|
);
|
||||||
}
|
} else {
|
||||||
} else if (ent.entityKind === 'SHOWCASE') {
|
citationsMap.set(ent.id, {
|
||||||
const dbShowcase = await prisma.contentShowcase.findUnique({
|
assetId: ent.id,
|
||||||
where: { id: ent.id }
|
title: ent.title,
|
||||||
});
|
location: 'Featured Content Showcase',
|
||||||
|
type: 'case_study',
|
||||||
if (dbShowcase) {
|
isRecommended: false,
|
||||||
citationsMap.set(dbShowcase.id, {
|
});
|
||||||
assetId: dbShowcase.id,
|
contextLines.push(
|
||||||
title: dbShowcase.title,
|
`[WORKBENCH ATTACHED FEATURED REEL] Title: "${ent.title}" | Video URL: "${ent.url || ''}" | Description:\n${ent.description || 'Interactive product reel'}\n`
|
||||||
location: 'Featured Content Showcase',
|
);
|
||||||
type: 'case_study',
|
}
|
||||||
isRecommended: false,
|
} else if (ent.entityKind === 'ECOSYSTEM') {
|
||||||
});
|
const dbOffering = await prisma.ecosystemOffering.findUnique({
|
||||||
|
where: { id: ent.id }
|
||||||
contextLines.push(
|
}).catch(() => null);
|
||||||
`[WORKBENCH ATTACHED FEATURED REEL] Title: "${dbShowcase.title}" | Video URL: "${dbShowcase.youtubeUrl}" | Description:\n${dbShowcase.description || 'Interactive product reel'}\n`
|
|
||||||
);
|
if (dbOffering) {
|
||||||
}
|
citationsMap.set(dbOffering.id, {
|
||||||
} else if (ent.entityKind === 'ECOSYSTEM') {
|
assetId: dbOffering.id,
|
||||||
const dbOffering = await prisma.ecosystemOffering.findUnique({
|
title: dbOffering.name,
|
||||||
where: { id: ent.id }
|
location: 'Ecosystem Offering',
|
||||||
});
|
type: 'offering',
|
||||||
|
isRecommended: false,
|
||||||
if (dbOffering) {
|
});
|
||||||
citationsMap.set(dbOffering.id, {
|
|
||||||
assetId: dbOffering.id,
|
contextLines.push(
|
||||||
title: dbOffering.name,
|
`[WORKBENCH ATTACHED ECOSYSTEM OFFERING] Name: "${dbOffering.name}" | Type: "${dbOffering.type}" | Tagline: "${dbOffering.tagline}" | Website URL: "${dbOffering.websiteUrl}" | Key Benefits: ${((dbOffering.benefits as string[]) || []).join('; ')} | Description:\n${dbOffering.description}\n`
|
||||||
location: 'Ecosystem Offering',
|
);
|
||||||
type: 'offering',
|
} else {
|
||||||
isRecommended: false,
|
citationsMap.set(ent.id, {
|
||||||
});
|
assetId: ent.id,
|
||||||
|
title: ent.title,
|
||||||
contextLines.push(
|
location: 'Ecosystem Offering',
|
||||||
`[WORKBENCH ATTACHED ECOSYSTEM OFFERING] Name: "${dbOffering.name}" | Type: "${dbOffering.type}" | Tagline: "${dbOffering.tagline}" | Website URL: "${dbOffering.websiteUrl}" | Description:\n${dbOffering.description}\n`
|
type: 'offering',
|
||||||
);
|
isRecommended: false,
|
||||||
}
|
});
|
||||||
} else if (ent.entityKind === 'LEGAL') {
|
contextLines.push(
|
||||||
const dbLegal = await prisma.legalDocument.findFirst({
|
`[WORKBENCH ATTACHED ECOSYSTEM OFFERING] Name: "${ent.title}" | Description:\n${ent.description || 'Enterprise Ecosystem Offering'}\n`
|
||||||
where: { OR: [{ id: ent.id }, { type: ent.title.includes('NDA') ? 'NDA' : 'MSA' }] }
|
);
|
||||||
});
|
}
|
||||||
|
} else if (ent.entityKind === 'LEGAL') {
|
||||||
if (dbLegal) {
|
const dbLegal = await prisma.legalDocument.findFirst({
|
||||||
citationsMap.set(dbLegal.id, {
|
where: { OR: [{ id: ent.id }, { type: ent.title.includes('NDA') ? 'NDA' : 'MSA' }] }
|
||||||
assetId: dbLegal.id,
|
}).catch(() => null);
|
||||||
title: ent.title,
|
|
||||||
location: 'Legal Agreement',
|
if (dbLegal) {
|
||||||
type: 'legal',
|
citationsMap.set(dbLegal.id, {
|
||||||
isRecommended: false,
|
assetId: dbLegal.id,
|
||||||
});
|
title: ent.title,
|
||||||
|
location: 'Legal Agreement',
|
||||||
contextLines.push(
|
type: 'legal',
|
||||||
`[WORKBENCH ATTACHED LEGAL AGREEMENT] Title: "${ent.title}" | Version: "${dbLegal.version}" | Content Summary:\n${dbLegal.content.slice(0, 800)}...\n`
|
isRecommended: false,
|
||||||
);
|
});
|
||||||
|
|
||||||
|
contextLines.push(
|
||||||
|
`[WORKBENCH ATTACHED LEGAL AGREEMENT] Type: "${dbLegal.type}" | Version: "${dbLegal.version}" | Content:\n${dbLegal.content}\n`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to parse entity payload in RAG pipeline', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -290,7 +328,7 @@ export class ChatService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
contextLines.push(
|
contextLines.push(
|
||||||
`[Ecosystem Offering] Name: "${eo.name}" | Type: "${eo.type}" | Tagline: "${eo.tagline}" | Website: "${eo.websiteUrl}" | Description:\n${eo.description}\n`
|
`[Ecosystem Offering] Name: "${eo.name}" | Type: "${eo.type}" | Tagline: "${eo.tagline}" | Website URL: "${eo.websiteUrl}" | Key Benefits: ${((eo.benefits as string[]) || []).join('; ')} | Description:\n${eo.description}\n`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -448,12 +486,19 @@ Exact Admin Console Layout Guide for Administrator (${user.email}):
|
|||||||
Role & Target Audience:
|
Role & Target Audience:
|
||||||
- User: ${user.email} (${isClient ? 'Client / Partner' : 'Portal Administrator'}).
|
- User: ${user.email} (${isClient ? 'Client / Partner' : 'Portal Administrator'}).
|
||||||
|
|
||||||
OUTPUT FORMATTING REQUIREMENTS (CRITICAL FOR IMMACULATE VISUAL STRUCTURE):
|
OUTPUT FORMATTING REQUIREMENTS (CRITICAL FOR QUALITY & SECURITY):
|
||||||
1. **Multi-Turn Context Awareness**: Maintain full conversational memory. When asked for follow-ups or comparisons of previously mentioned assets, resolve references accurately.
|
1. **Never Output Database IDs or UUIDs**: Database IDs (UUIDs, hashes, etc.) are strictly confidential internal identifiers. You MUST NEVER output any ID or UUID in your response text to the user.
|
||||||
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 | | :--- | :--- |".
|
2. **Strict Guidelines for Suggested Links**:
|
||||||
3. **Structure Sections Clearly**: Use bold section titles (e.g. "### Summary", "### Key Differences", "### Features & Specifications").
|
- **Internal Portal Pages**: If referencing pages inside the portal, you must ONLY use these exact human-accessible relative links:
|
||||||
4. **Use Bullet Points for Readability**: When detailing lists of features, target users, or tech stacks, use bulleted lists instead of long unformatted paragraphs.
|
- Client Portal: \`/client/assets\` (Asset Explorer), \`/client/showcase\` (Featured Content), \`/client/ecosystem\` (Explore More), or \`/client/agreements\` (Legal Agreements).
|
||||||
5. **Clickable Links**: Include direct clickable Markdown links if URLs or resources exist (e.g., "🔗 **Direct Link**: [Visit Resource](URL)").
|
- Admin Console: \`/admin/assets\` (Manage Catalog), \`/admin/showcase\` (Showcase Manager), \`/admin/ecosystem\` (Ecosystem Manager), or \`/admin/legal\` (Legal Templates).
|
||||||
|
- **CRITICAL**: Never append database IDs or UUIDs to these paths (e.g., do NOT link to \`/client/assets/123-abc\`). Doing so creates broken pages.
|
||||||
|
- **External Links**: You may suggest an external resource link (e.g. "[Visit Website](URL)") ONLY if the URL starts with \`http\` or \`https\` and is a public web link (not containing \`localhost\`, \`127.0.0.1\`, or \`/uploads/\`).
|
||||||
|
- **Fallback**: If no valid link exists, instruct the user to view it via the citation cards below the chat bubble using the **Quick Preview** button or locate it in the catalog.
|
||||||
|
3. **Multi-Turn Context Awareness**: Maintain full conversational memory. When asked for follow-ups or comparisons of previously mentioned assets, resolve references accurately.
|
||||||
|
4. **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 | | :--- | :--- |".
|
||||||
|
5. **Structure Sections Clearly**: Use bold section titles (e.g. "### Summary", "### Key Differences", "### Features & Specifications").
|
||||||
|
6. **Use Bullet Points for Readability**: When detailing lists of features, target users, or tech stacks, use bulleted lists instead of long unformatted paragraphs.
|
||||||
|
|
||||||
${hasAttachedEntities ? `
|
${hasAttachedEntities ? `
|
||||||
CRITICAL RULES FOR WORKBENCH ATTACHED ENTITIES:
|
CRITICAL RULES FOR WORKBENCH ATTACHED ENTITIES:
|
||||||
@ -462,8 +507,10 @@ CRITICAL RULES FOR WORKBENCH ATTACHED ENTITIES:
|
|||||||
- Provide a clear, high-impact summary of what these attached items are, their core problem/solution, tech stack, and key features.
|
- 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.
|
- 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.
|
CRITICAL GROUNDING RULES (ZERO HALLUCINATION & STRICT KNOWLEDGE COMPLIANCE):
|
||||||
- Do NOT hallucinate or list random showcase reels or unrelated assets unless explicitly requested by the user.
|
1. **Rely ONLY on Provided Knowledge Context**: Answer user questions 100% EXCLUSIVELY using the provided Knowledge Context. Do NOT use outside general knowledge or make ungrounded assumptions.
|
||||||
|
2. **No Invented Demos or Non-Working Links**: If the context does not explicitly list a live demo URL or document file link for an offering (such as CodeNuk, Cloudtopiaa, or Audittrax Labs), state clearly: "You can explore this offering under the 'Explore More' section (/client/ecosystem) or visit their website at [Website URL]." Do NOT invent dummy demo links, raw file paths, or non-working URLs.
|
||||||
|
3. **If Information is Missing**: If the provided Knowledge Context does not contain the answer, explicitly state: "I don't have detailed information on that in the portal database. Please check the Asset Explorer or contact your account administrator."
|
||||||
`}
|
`}
|
||||||
|
|
||||||
Portal Navigation Guidelines:
|
Portal Navigation Guidelines:
|
||||||
@ -559,10 +606,35 @@ Portal Navigation Guidelines:
|
|||||||
assistantReply = 'Here is the requested information from your shared catalog:\n\n' + contextBlock;
|
assistantReply = 'Here is the requested information from your shared catalog:\n\n' + contextBlock;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strict Citation Filtering: Only include Verified Knowledge Source citations if user explicitly attached entities or asked for asset recommendations
|
// Strict Citation Filtering: Only include Verified Knowledge Source citations if user explicitly attached entities or asked for asset/resource 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');
|
const isGeneralHelpQuery = promptLower.includes('password') ||
|
||||||
|
promptLower.includes('theme') ||
|
||||||
|
promptLower.includes('appearance') ||
|
||||||
|
promptLower.includes('profile') ||
|
||||||
|
promptLower.includes('how to log') ||
|
||||||
|
promptLower.includes('how to sign') ||
|
||||||
|
promptLower.includes('help');
|
||||||
|
|
||||||
const finalCitations = (hasAttachedEntities || isExplicitSearchQuery)
|
const hasAssetKeywords = promptLower.includes('asset') ||
|
||||||
|
promptLower.includes('recommend') ||
|
||||||
|
promptLower.includes('find') ||
|
||||||
|
promptLower.includes('search') ||
|
||||||
|
promptLower.includes('show') ||
|
||||||
|
promptLower.includes('list') ||
|
||||||
|
promptLower.includes('what are') ||
|
||||||
|
promptLower.includes('is there') ||
|
||||||
|
promptLower.includes('are there') ||
|
||||||
|
promptLower.includes('showcase') ||
|
||||||
|
promptLower.includes('agreement') ||
|
||||||
|
promptLower.includes('legal') ||
|
||||||
|
promptLower.includes('document') ||
|
||||||
|
promptLower.includes('video') ||
|
||||||
|
promptLower.includes('reel') ||
|
||||||
|
promptLower.includes('offering') ||
|
||||||
|
promptLower.includes('which are') ||
|
||||||
|
promptLower.includes('tell me more');
|
||||||
|
|
||||||
|
const finalCitations = (hasAttachedEntities || (hasAssetKeywords && !isGeneralHelpQuery))
|
||||||
? Array.from(citationsMap.values())
|
? Array.from(citationsMap.values())
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ dotenv.config();
|
|||||||
|
|
||||||
import prisma from './db';
|
import prisma from './db';
|
||||||
|
|
||||||
async function seedTaxonomy() {
|
export async function seedFourGroupTaxonomy() {
|
||||||
console.log('[Taxonomy-Seed] Starting taxonomy re-seeding with exact user specifications...');
|
console.log('[Taxonomy-Seed] Starting taxonomy re-seeding with exact user specifications...');
|
||||||
|
|
||||||
await prisma.vertical.deleteMany();
|
await prisma.vertical.deleteMany();
|
||||||
@ -200,10 +200,13 @@ async function seedTaxonomy() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[Taxonomy-Seed] Successfully mapped exact taxonomy relations for ${updatedCount} assets.`);
|
console.log(`[Taxonomy-Seed] Successfully mapped exact taxonomy relations for ${updatedCount} assets.`);
|
||||||
process.exit(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
seedTaxonomy().catch(err => {
|
if (require.main === module || (process.argv[1] && process.argv[1].includes('seed-taxonomy'))) {
|
||||||
console.error('[Taxonomy-Seed] Failed:', err);
|
seedFourGroupTaxonomy()
|
||||||
process.exit(1);
|
.then(() => process.exit(0))
|
||||||
});
|
.catch(err => {
|
||||||
|
console.error('[Taxonomy-Seed] Failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -5,6 +5,9 @@
|
|||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Tech4Biz Client & Admin Portal</title>
|
<title>Tech4Biz Client & Admin Portal</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
600
Channel-Frontend/package-lock.json
generated
600
Channel-Frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -16,27 +16,27 @@ export const PageLayout: React.FC<PageLayoutProps> = ({
|
|||||||
className = '',
|
className = '',
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-col h-[calc(100vh-140px)] md:h-[calc(100vh-160px)] w-full overflow-hidden ${className}`}>
|
<div className={`flex-1 flex flex-col min-h-0 h-full w-full overflow-hidden ${className}`}>
|
||||||
{/* Page Header (Fixed) */}
|
{/* Page Header (Fixed) */}
|
||||||
<div className="shrink-0 mb-4">
|
<div className="shrink-0 mb-3 sm:mb-4">
|
||||||
{header}
|
{header}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Toolbar (Fixed) */}
|
{/* Toolbar (Fixed) */}
|
||||||
{toolbar && (
|
{toolbar && (
|
||||||
<div className="shrink-0 mb-4">
|
<div className="shrink-0 mb-3 sm:mb-4">
|
||||||
{toolbar}
|
{toolbar}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Scrollable Content Area */}
|
{/* Scrollable Content Area */}
|
||||||
<div className="flex-1 min-h-0 overflow-y-auto bg-ink-0 border border-ink-200 rounded-xl shadow-sm relative flex flex-col scrollbar-thin">
|
<div className="flex-1 min-h-0 overflow-y-auto bg-ink-0 border border-ink-200 rounded-2xl shadow-sm relative flex flex-col scrollbar-thin">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Page-level Footer/Pagination (Fixed) */}
|
{/* Page-level Footer/Pagination (Fixed) */}
|
||||||
{footer && (
|
{footer && (
|
||||||
<div className="shrink-0 mt-4">
|
<div className="shrink-0 mt-3 sm:mt-4">
|
||||||
{footer}
|
{footer}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -66,13 +66,25 @@ export const ChatDrawer: React.FC = () => {
|
|||||||
const [messages, setMessages] = useState<ChatMessage[]>([
|
const [messages, setMessages] = useState<ChatMessage[]>([
|
||||||
{
|
{
|
||||||
sender: 'ASSISTANT',
|
sender: 'ASSISTANT',
|
||||||
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!',
|
content: 'Hello! I am your **Tech4Biz AI Advisor Workbench**. **Drag and drop any asset, case study reel, ecosystem offering** here to inspect, summarize, and receive instant role-tailored explanations!',
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
const [activePreviewAsset, setActivePreviewAsset] = useState<Asset | null>(null);
|
const [activePreviewAsset, setActivePreviewAsset] = useState<Asset | null>(null);
|
||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [hasActiveOverlay, setHasActiveOverlay] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkOverlays = () => {
|
||||||
|
const overlays = document.querySelectorAll('.fixed.inset-0.z-50, .fixed.inset-y-0.right-0');
|
||||||
|
setHasActiveOverlay(overlays.length > 0);
|
||||||
|
};
|
||||||
|
checkOverlays();
|
||||||
|
const interval = setInterval(checkOverlays, 300);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
@ -328,7 +340,7 @@ export const ChatDrawer: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Draggable Floating Trigger Pill */}
|
{/* Draggable Floating Trigger Pill */}
|
||||||
{!isOpen && (
|
{!isOpen && !hasActiveOverlay && (
|
||||||
<motion.button
|
<motion.button
|
||||||
drag
|
drag
|
||||||
dragConstraints={{ left: -1200, right: 20, top: -800, bottom: 20 }}
|
dragConstraints={{ left: -1200, right: 20, top: -800, bottom: 20 }}
|
||||||
@ -339,10 +351,10 @@ export const ChatDrawer: React.FC = () => {
|
|||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
onClick={() => { setIsOpen(true); setIsMinimized(false); }}
|
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-grab active:cursor-grabbing group select-none"
|
className="fixed bottom-14 right-6 sm:bottom-16 sm:right-8 z-30 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">
|
<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" />
|
<Bot className="w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-extrabold tracking-wide font-sans">
|
<span className="text-xs font-extrabold tracking-wide font-sans">
|
||||||
AI Advisor
|
AI Advisor
|
||||||
@ -362,9 +374,9 @@ export const ChatDrawer: React.FC = () => {
|
|||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
initial={{ opacity: 0, y: 40, scale: 0.95 }}
|
initial={{ opacity: 0, y: 40, scale: 0.95 }}
|
||||||
animate={{
|
animate={{
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
y: 0,
|
y: 0,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
height: dimensions.height,
|
height: dimensions.height,
|
||||||
width: dimensions.width
|
width: dimensions.width
|
||||||
@ -381,7 +393,7 @@ export const ChatDrawer: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-bold text-xs text-white">Tech4Biz AI Advisor</h3>
|
<h3 className="font-bold text-xs text-white">Tech4Biz AI Advisor</h3>
|
||||||
<p className="text-[10px] text-slate-400">Powered by DeepSeek LLM Engine</p>
|
<p className="text-[10px] text-slate-400">Enterprise Intelligent Assistant</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -405,11 +417,10 @@ export const ChatDrawer: React.FC = () => {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleToggleHistory}
|
onClick={handleToggleHistory}
|
||||||
title="Past Chat Sessions"
|
title="Past Chat Sessions"
|
||||||
className={`p-1.5 rounded-lg border transition-colors cursor-pointer ${
|
className={`p-1.5 rounded-lg border transition-colors cursor-pointer ${showHistory
|
||||||
showHistory
|
|
||||||
? 'bg-emerald-500/30 text-emerald-300 border-emerald-500/50'
|
? 'bg-emerald-500/30 text-emerald-300 border-emerald-500/50'
|
||||||
: 'text-slate-400 hover:text-white hover:bg-slate-800 border-slate-800'
|
: 'text-slate-400 hover:text-white hover:bg-slate-800 border-slate-800'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<History className="w-3.5 h-3.5" />
|
<History className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
@ -433,7 +444,7 @@ export const ChatDrawer: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{isMinimized ? <Maximize2 className="w-3.5 h-3.5" /> : <Minimize2 className="w-3.5 h-3.5" />}
|
{isMinimized ? <Maximize2 className="w-3.5 h-3.5" /> : <Minimize2 className="w-3.5 h-3.5" />}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Close Button */}
|
{/* Close Button */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
@ -467,11 +478,10 @@ export const ChatDrawer: React.FC = () => {
|
|||||||
<div
|
<div
|
||||||
key={sess.id}
|
key={sess.id}
|
||||||
onClick={() => handleLoadSession(sess.id)}
|
onClick={() => handleLoadSession(sess.id)}
|
||||||
className={`p-2 rounded-xl border text-xs cursor-pointer flex items-center justify-between transition-all ${
|
className={`p-2 rounded-xl border text-xs cursor-pointer flex items-center justify-between transition-all ${isActive
|
||||||
isActive
|
? 'bg-emerald-500/20 text-emerald-300 border-emerald-500/40'
|
||||||
? 'bg-emerald-500/20 text-emerald-300 border-emerald-500/40'
|
|
||||||
: 'bg-slate-950/70 hover:bg-slate-800 text-slate-300 border-slate-800'
|
: 'bg-slate-950/70 hover:bg-slate-800 text-slate-300 border-slate-800'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
<MessageSquare className="w-3.5 h-3.5 text-emerald-400 shrink-0" />
|
<MessageSquare className="w-3.5 h-3.5 text-emerald-400 shrink-0" />
|
||||||
@ -497,20 +507,18 @@ export const ChatDrawer: React.FC = () => {
|
|||||||
key={i}
|
key={i}
|
||||||
className={`flex gap-2.5 ${msg.sender === 'USER' ? 'flex-row-reverse' : 'flex-row'}`}
|
className={`flex gap-2.5 ${msg.sender === 'USER' ? 'flex-row-reverse' : 'flex-row'}`}
|
||||||
>
|
>
|
||||||
<div className={`p-1.5 rounded-xl shrink-0 h-fit ${
|
<div className={`p-1.5 rounded-xl shrink-0 h-fit ${msg.sender === 'USER'
|
||||||
msg.sender === 'USER'
|
? 'bg-emerald-600 text-white'
|
||||||
? 'bg-emerald-600 text-white'
|
|
||||||
: 'bg-slate-900 text-emerald-400 border border-slate-700'
|
: 'bg-slate-900 text-emerald-400 border border-slate-700'
|
||||||
}`}>
|
}`}>
|
||||||
{msg.sender === 'USER' ? <UserIcon className="w-3.5 h-3.5" /> : <Bot className="w-3.5 h-3.5" />}
|
{msg.sender === 'USER' ? <UserIcon className="w-3.5 h-3.5" /> : <Bot className="w-3.5 h-3.5" />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`space-y-2 max-w-[85%] ${msg.sender === 'USER' ? 'text-right' : 'text-left'}`}>
|
<div className={`space-y-2 max-w-[85%] ${msg.sender === 'USER' ? 'text-right' : 'text-left'}`}>
|
||||||
<div className={`p-3.5 rounded-2xl leading-relaxed ${
|
<div className={`p-3.5 rounded-2xl leading-relaxed ${msg.sender === 'USER'
|
||||||
msg.sender === 'USER'
|
|
||||||
? 'bg-emerald-600 text-white font-medium rounded-tr-none whitespace-pre-wrap'
|
? 'bg-emerald-600 text-white font-medium rounded-tr-none whitespace-pre-wrap'
|
||||||
: 'bg-slate-900 text-slate-100 border border-slate-800 rounded-tl-none shadow-md'
|
: 'bg-slate-900 text-slate-100 border border-slate-800 rounded-tl-none shadow-md'
|
||||||
}`}>
|
}`}>
|
||||||
{msg.sender === 'USER' ? (
|
{msg.sender === 'USER' ? (
|
||||||
msg.content
|
msg.content
|
||||||
) : (
|
) : (
|
||||||
@ -583,7 +591,7 @@ export const ChatDrawer: React.FC = () => {
|
|||||||
{loading && (
|
{loading && (
|
||||||
<div className="flex items-center gap-2.5 text-slate-400 text-xs italic p-2">
|
<div className="flex items-center gap-2.5 text-slate-400 text-xs italic p-2">
|
||||||
<RefreshCw className="w-3.5 h-3.5 animate-spin text-emerald-400" />
|
<RefreshCw className="w-3.5 h-3.5 animate-spin text-emerald-400" />
|
||||||
Analyzing catalog knowledge with DeepSeek...
|
Analyzing catalog knowledge...
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -670,7 +678,7 @@ export const ChatDrawer: React.FC = () => {
|
|||||||
asset={activePreviewAsset}
|
asset={activePreviewAsset}
|
||||||
isOpen={!!activePreviewAsset}
|
isOpen={!!activePreviewAsset}
|
||||||
user={user}
|
user={user}
|
||||||
onDownload={() => {}}
|
onDownload={() => { }}
|
||||||
onClose={() => setActivePreviewAsset(null)}
|
onClose={() => setActivePreviewAsset(null)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -213,12 +213,37 @@ const handleLine = (line: string, state: ParseState): void => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preprocesses raw markdown text to split single-line concatenated markdown table rows into proper multi-line Markdown tables.
|
* Preprocesses raw markdown text to split single-line concatenated markdown table rows and merge split table rows across lines.
|
||||||
*/
|
*/
|
||||||
const sanitizeMarkdownText = (rawText: string): string => {
|
const sanitizeMarkdownText = (rawText: string): string => {
|
||||||
if (!rawText) return "";
|
if (!rawText) return "";
|
||||||
|
|
||||||
let formatted = rawText;
|
const lines = rawText.split("\n");
|
||||||
|
const processedLines: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
// If the line starts with a pipe but doesn't end with one, it is likely split across linebreaks
|
||||||
|
if (trimmed.startsWith("|") && !trimmed.endsWith("|")) {
|
||||||
|
let merged = line;
|
||||||
|
while (i + 1 < lines.length) {
|
||||||
|
const nextLine = lines[i + 1];
|
||||||
|
const nextTrimmed = nextLine.trim();
|
||||||
|
merged += " " + nextTrimmed;
|
||||||
|
i++;
|
||||||
|
if (nextTrimmed.endsWith("|")) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processedLines.push(merged);
|
||||||
|
} else {
|
||||||
|
processedLines.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatted = processedLines.join("\n");
|
||||||
|
|
||||||
// Split inline concatenated table rows like "| Col 1 | Col 2 | | :--- | :--- | | Val 1 | Val 2 |"
|
// 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*:-/g, "|\n| :-");
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import {
|
import {
|
||||||
FileText,
|
FileText,
|
||||||
File,
|
File,
|
||||||
Eye,
|
Eye,
|
||||||
MoreVertical,
|
MoreVertical,
|
||||||
Edit3,
|
Edit3,
|
||||||
Share2,
|
Share2,
|
||||||
Trash2,
|
Trash2,
|
||||||
Lock,
|
Lock,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Download,
|
Download,
|
||||||
Clock,
|
Clock,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Globe,
|
Globe,
|
||||||
Sparkles
|
Sparkles
|
||||||
@ -85,29 +85,29 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
|
|
||||||
|
|
||||||
const isRenderable = (type: string, url: string) => {
|
const isRenderable = (type: string, url: string) => {
|
||||||
const isOffice = type.includes('word') || type.includes('presentation') || type.includes('sheet') ||
|
const isOffice = type.includes('word') || type.includes('presentation') || type.includes('sheet') ||
|
||||||
url.toLowerCase().endsWith('.docx') || url.toLowerCase().endsWith('.doc') ||
|
url.toLowerCase().endsWith('.docx') || url.toLowerCase().endsWith('.doc') ||
|
||||||
url.toLowerCase().endsWith('.pptx') || url.toLowerCase().endsWith('.ppt') ||
|
url.toLowerCase().endsWith('.pptx') || url.toLowerCase().endsWith('.ppt') ||
|
||||||
url.toLowerCase().endsWith('.xlsx') || url.toLowerCase().endsWith('.xls') ||
|
url.toLowerCase().endsWith('.xlsx') || url.toLowerCase().endsWith('.xls') ||
|
||||||
url.toLowerCase().endsWith('.csv') || type.includes('csv');
|
url.toLowerCase().endsWith('.csv') || type.includes('csv');
|
||||||
|
|
||||||
const isTextOrCode = url.toLowerCase().endsWith('.md') ||
|
const isTextOrCode = url.toLowerCase().endsWith('.md') ||
|
||||||
url.toLowerCase().endsWith('.txt') ||
|
url.toLowerCase().endsWith('.txt') ||
|
||||||
url.toLowerCase().endsWith('.json') ||
|
url.toLowerCase().endsWith('.json') ||
|
||||||
url.toLowerCase().endsWith('.js') ||
|
url.toLowerCase().endsWith('.js') ||
|
||||||
url.toLowerCase().endsWith('.ts') ||
|
url.toLowerCase().endsWith('.ts') ||
|
||||||
url.toLowerCase().endsWith('.tsx') ||
|
url.toLowerCase().endsWith('.tsx') ||
|
||||||
url.toLowerCase().endsWith('.jsx') ||
|
url.toLowerCase().endsWith('.jsx') ||
|
||||||
url.toLowerCase().endsWith('.py') ||
|
url.toLowerCase().endsWith('.py') ||
|
||||||
url.toLowerCase().endsWith('.yaml') ||
|
url.toLowerCase().endsWith('.yaml') ||
|
||||||
url.toLowerCase().endsWith('.yml') ||
|
url.toLowerCase().endsWith('.yml') ||
|
||||||
url.toLowerCase().endsWith('.css') ||
|
url.toLowerCase().endsWith('.css') ||
|
||||||
url.toLowerCase().endsWith('.html') ||
|
url.toLowerCase().endsWith('.html') ||
|
||||||
url.toLowerCase().endsWith('.sh') ||
|
url.toLowerCase().endsWith('.sh') ||
|
||||||
type.includes('text') ||
|
type.includes('text') ||
|
||||||
type.includes('markdown') ||
|
type.includes('markdown') ||
|
||||||
type.includes('json') ||
|
type.includes('json') ||
|
||||||
type.includes('javascript');
|
type.includes('javascript');
|
||||||
|
|
||||||
return type === 'url' || type.includes('pdf') || type.includes('image') || type.includes('png') || type.includes('jpg') || isOffice || isTextOrCode;
|
return type === 'url' || type.includes('pdf') || type.includes('image') || type.includes('png') || type.includes('jpg') || isOffice || isTextOrCode;
|
||||||
};
|
};
|
||||||
@ -138,7 +138,7 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
const isWord = asset.type.includes('word') || asset.url.toLowerCase().endsWith('.docx') || asset.url.toLowerCase().endsWith('.doc');
|
const isWord = asset.type.includes('word') || asset.url.toLowerCase().endsWith('.docx') || asset.url.toLowerCase().endsWith('.doc');
|
||||||
const isPresentation = asset.type.includes('presentation') || asset.url.toLowerCase().endsWith('.pptx') || asset.url.toLowerCase().endsWith('.ppt');
|
const isPresentation = asset.type.includes('presentation') || asset.url.toLowerCase().endsWith('.pptx') || asset.url.toLowerCase().endsWith('.ppt');
|
||||||
const isSpreadsheet = asset.type.includes('sheet') || asset.url.toLowerCase().endsWith('.xlsx') || asset.url.toLowerCase().endsWith('.xls') || asset.url.toLowerCase().endsWith('.csv');
|
const isSpreadsheet = asset.type.includes('sheet') || asset.url.toLowerCase().endsWith('.xlsx') || asset.url.toLowerCase().endsWith('.xls') || asset.url.toLowerCase().endsWith('.csv');
|
||||||
|
|
||||||
const hasBanner = isImage || !!asset.thumbnailUrl;
|
const hasBanner = isImage || !!asset.thumbnailUrl;
|
||||||
const bannerSrc = asset.thumbnailUrl ? asset.thumbnailUrl : asset.url;
|
const bannerSrc = asset.thumbnailUrl ? asset.thumbnailUrl : asset.url;
|
||||||
|
|
||||||
@ -160,7 +160,7 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
id={`asset-card-${asset.id}`}
|
id={`asset-card-${asset.id}`}
|
||||||
layout
|
layout
|
||||||
draggable={true}
|
draggable={true}
|
||||||
@ -188,13 +188,12 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
}
|
}
|
||||||
onViewDetails(asset);
|
onViewDetails(asset);
|
||||||
}}
|
}}
|
||||||
className={`group bg-ink-0 border rounded-xl p-4 transition-[border-color,box-shadow,background-color] duration-300 flex flex-col justify-between cursor-pointer min-h-[410px] ${
|
className={`group bg-ink-0 border rounded-xl p-4 transition-[border-color,box-shadow,background-color] duration-300 flex flex-col justify-between cursor-pointer min-h-[410px] ${isExpanded
|
||||||
isExpanded
|
|
||||||
? 'absolute z-20 top-0 left-0 right-0 h-auto shadow-2xl border-ink-300 bg-ink-0'
|
? 'absolute z-20 top-0 left-0 right-0 h-auto shadow-2xl border-ink-300 bg-ink-0'
|
||||||
: isRecommended
|
: isRecommended
|
||||||
? 'relative w-full h-full border-amber-500/35 bg-gradient-to-br from-amber-500/[0.02] via-ink-0 to-ink-0 hover:border-amber-500 hover:shadow-lg hover:shadow-amber-500/10 hover:-translate-y-0.5'
|
? 'relative w-full h-full border-amber-500/35 bg-gradient-to-br from-amber-500/[0.02] via-ink-0 to-ink-0 hover:border-amber-500 hover:shadow-lg hover:shadow-amber-500/10 hover:-translate-y-0.5'
|
||||||
: 'relative w-full h-full border-ink-200 hover:border-ink-300 hover:shadow-md hover:-translate-y-0.5'
|
: 'relative w-full h-full border-ink-200 hover:border-ink-300 hover:shadow-md hover:-translate-y-0.5'
|
||||||
} ${isSelected ? 'border-ink-900 ring-1 ring-ink-900 bg-ink-50/30' : ''}`}
|
} ${isSelected ? 'border-ink-900 ring-1 ring-ink-900 bg-ink-50/30' : ''}`}
|
||||||
>
|
>
|
||||||
<div className="flex-grow flex flex-col">
|
<div className="flex-grow flex flex-col">
|
||||||
{/* Visual Thumbnail Area */}
|
{/* Visual Thumbnail Area */}
|
||||||
@ -207,9 +206,8 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onChange={(e) => onToggleSelect(asset.id, e as unknown as React.MouseEvent)}
|
onChange={(e) => onToggleSelect(asset.id, e as unknown as React.MouseEvent)}
|
||||||
className={`w-3.5 h-3.5 rounded border-ink-300 bg-ink-0 text-ink-900 focus:ring-ink-900/10 cursor-pointer shadow-sm transition-opacity duration-200 ${
|
className={`w-3.5 h-3.5 rounded border-ink-300 bg-ink-0 text-ink-900 focus:ring-ink-900/10 cursor-pointer shadow-sm transition-opacity duration-200 ${isSelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
|
||||||
isSelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
|
}`}
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isRecommended && (
|
{isRecommended && (
|
||||||
@ -227,11 +225,11 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
title="Inspect with AI Advisor Workbench"
|
title="Inspect with AI Advisor Workbench"
|
||||||
>
|
>
|
||||||
<Sparkles className="w-3 h-3 text-amber-400 fill-amber-400" />
|
<Sparkles className="w-3 h-3 text-amber-400 fill-amber-400" />
|
||||||
<span>AI Workbench</span>
|
<span>ask AI</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isRenderable(asset.type, asset.url) && (
|
{isRenderable(asset.type, asset.url) && (
|
||||||
<button
|
<button
|
||||||
onClick={() => onOpenViewer(asset)}
|
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 cursor-pointer"
|
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"
|
title="Preview Online"
|
||||||
@ -241,18 +239,18 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveMenuId(isMenuOpen ? null : asset.id)}
|
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 cursor-pointer"
|
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" />
|
<MoreVertical className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isMenuOpen && (
|
{isMenuOpen && (
|
||||||
<>
|
<>
|
||||||
<div className="fixed inset-0 z-10" onClick={() => setActiveMenuId(null)} />
|
<div className="fixed inset-0 z-10" onClick={() => setActiveMenuId(null)} />
|
||||||
<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">
|
<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
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
handleInspectWithAI(e);
|
handleInspectWithAI(e);
|
||||||
setActiveMenuId(null);
|
setActiveMenuId(null);
|
||||||
@ -262,7 +260,7 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
<Sparkles className="w-3.5 h-3.5 text-amber-500 fill-amber-500" />
|
<Sparkles className="w-3.5 h-3.5 text-amber-500 fill-amber-500" />
|
||||||
Inspect with AI Advisor
|
Inspect with AI Advisor
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onViewDetails(asset);
|
onViewDetails(asset);
|
||||||
setActiveMenuId(null);
|
setActiveMenuId(null);
|
||||||
@ -274,7 +272,7 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
{user?.role === 'ADMIN' && (
|
{user?.role === 'ADMIN' && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onEdit(asset);
|
onEdit(asset);
|
||||||
setActiveMenuId(null);
|
setActiveMenuId(null);
|
||||||
@ -284,7 +282,7 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
<Edit3 className="w-3.5 h-3.5" />
|
<Edit3 className="w-3.5 h-3.5" />
|
||||||
Edit Asset
|
Edit Asset
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onShare(asset);
|
onShare(asset);
|
||||||
setActiveMenuId(null);
|
setActiveMenuId(null);
|
||||||
@ -294,7 +292,7 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
<Share2 className="w-3.5 h-3.5" />
|
<Share2 className="w-3.5 h-3.5" />
|
||||||
Share Settings
|
Share Settings
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onDelete(asset.id);
|
onDelete(asset.id);
|
||||||
setActiveMenuId(null);
|
setActiveMenuId(null);
|
||||||
@ -314,9 +312,9 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
|
|
||||||
{/* Thumbnail Preview Area Content */}
|
{/* Thumbnail Preview Area Content */}
|
||||||
{hasBanner ? (
|
{hasBanner ? (
|
||||||
<img
|
<img
|
||||||
src={getFullAssetUrl(bannerSrc)}
|
src={getFullAssetUrl(bannerSrc)}
|
||||||
alt={asset.title}
|
alt={asset.title}
|
||||||
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
e.currentTarget.style.display = 'none';
|
e.currentTarget.style.display = 'none';
|
||||||
@ -325,9 +323,9 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : isPdf ? (
|
) : isPdf ? (
|
||||||
<PdfThumbnail
|
<PdfThumbnail
|
||||||
url={getFullAssetUrl(asset.url)}
|
url={getFullAssetUrl(asset.url)}
|
||||||
title={asset.title}
|
title={asset.title}
|
||||||
fallback={
|
fallback={
|
||||||
<div className="flex flex-col items-center justify-center w-full h-full p-4 relative bg-red-500/[0.03] hover:bg-red-500/[0.06] transition-colors">
|
<div className="flex flex-col items-center justify-center w-full h-full p-4 relative bg-red-500/[0.03] hover:bg-red-500/[0.06] transition-colors">
|
||||||
<div className="w-[105px] h-[135px] bg-ink-0 border border-red-500/20 shadow-md rounded-lg flex flex-col justify-between p-3 relative overflow-hidden transition-all duration-300 group-hover:scale-105">
|
<div className="w-[105px] h-[135px] bg-ink-0 border border-red-500/20 shadow-md rounded-lg flex flex-col justify-between p-3 relative overflow-hidden transition-all duration-300 group-hover:scale-105">
|
||||||
@ -435,12 +433,12 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
<span className="truncate">{getFriendlyHostname(asset.url)}</span>
|
<span className="truncate">{getFriendlyHostname(asset.url)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Browser Body Web Mockup */}
|
{/* Browser Body Web Mockup */}
|
||||||
<div className="flex-1 bg-slate-950 p-3 flex flex-col justify-between relative overflow-hidden">
|
<div className="flex-1 bg-slate-950 p-3 flex flex-col justify-between relative overflow-hidden">
|
||||||
{/* Decorative Grid Pattern */}
|
{/* Decorative Grid Pattern */}
|
||||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#8080800a_1px,transparent_1px),linear-gradient(to_bottom,#8080800a_1px,transparent_1px)] bg-[size:10px_10px]" />
|
<div className="absolute inset-0 bg-[linear-gradient(to_right,#8080800a_1px,transparent_1px),linear-gradient(to_bottom,#8080800a_1px,transparent_1px)] bg-[size:10px_10px]" />
|
||||||
|
|
||||||
{/* Mock Web Page Hero Abstract Layout */}
|
{/* Mock Web Page Hero Abstract Layout */}
|
||||||
<div className="space-y-2 mt-1.5 relative z-10">
|
<div className="space-y-2 mt-1.5 relative z-10">
|
||||||
{/* Mock Navbar */}
|
{/* Mock Navbar */}
|
||||||
@ -476,8 +474,8 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
id={`fallback-${asset.id}`}
|
id={`fallback-${asset.id}`}
|
||||||
className="w-full h-full flex items-center justify-center bg-ink-50 relative overflow-hidden"
|
className="w-full h-full flex items-center justify-center bg-ink-50 relative overflow-hidden"
|
||||||
>
|
>
|
||||||
{isPresentation ? (
|
{isPresentation ? (
|
||||||
@ -523,13 +521,13 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
<div className="w-full h-full bg-white border border-slate-200 text-left select-none relative overflow-hidden p-3.5 flex flex-col justify-between">
|
<div className="w-full h-full bg-white border border-slate-200 text-left select-none relative overflow-hidden p-3.5 flex flex-col justify-between">
|
||||||
{/* Document Margins decorative elements */}
|
{/* Document Margins decorative elements */}
|
||||||
<div className="absolute top-0 right-0 left-0 h-1 bg-slate-800" />
|
<div className="absolute top-0 right-0 left-0 h-1 bg-slate-800" />
|
||||||
|
|
||||||
{/* Word Header */}
|
{/* Word Header */}
|
||||||
<div className="flex justify-between items-center text-[7px] text-slate-400 font-bold tracking-wide border-b border-slate-100 pb-1.5">
|
<div className="flex justify-between items-center text-[7px] text-slate-400 font-bold tracking-wide border-b border-slate-100 pb-1.5">
|
||||||
<span>PARTNER ASSET LIBRARY</span>
|
<span>PARTNER ASSET LIBRARY</span>
|
||||||
<span className="font-extrabold text-slate-800">DOCX</span>
|
<span className="font-extrabold text-slate-800">DOCX</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Page Title & Body Text Mockup */}
|
{/* Page Title & Body Text Mockup */}
|
||||||
<div className="space-y-2 flex-grow mt-3">
|
<div className="space-y-2 flex-grow mt-3">
|
||||||
<h4 className="text-[11px] font-extrabold text-slate-900 leading-snug font-sans line-clamp-2">
|
<h4 className="text-[11px] font-extrabold text-slate-900 leading-snug font-sans line-clamp-2">
|
||||||
@ -542,7 +540,7 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
<div className="h-1.5 bg-slate-50 rounded w-3/4" />
|
<div className="h-1.5 bg-slate-50 rounded w-3/4" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Signature / Metadata stamp at the bottom */}
|
{/* Signature / Metadata stamp at the bottom */}
|
||||||
<div className="flex items-center justify-between text-[7px] text-slate-400 font-bold border-t border-slate-100 pt-1.5 mt-auto font-mono">
|
<div className="flex items-center justify-between text-[7px] text-slate-400 font-bold border-t border-slate-100 pt-1.5 mt-auto font-mono">
|
||||||
<span>PAGE 1 OF 1</span>
|
<span>PAGE 1 OF 1</span>
|
||||||
@ -556,7 +554,7 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
<span className="text-[8px] font-extrabold text-white tracking-wider">Spreadsheet Editor</span>
|
<span className="text-[8px] font-extrabold text-white tracking-wider">Spreadsheet Editor</span>
|
||||||
<span className="text-[6px] font-extrabold text-emerald-100 bg-emerald-800/80 px-1 py-0.5 rounded uppercase">XLSX</span>
|
<span className="text-[6px] font-extrabold text-emerald-100 bg-emerald-800/80 px-1 py-0.5 rounded uppercase">XLSX</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sheet Grid Layout */}
|
{/* Sheet Grid Layout */}
|
||||||
<div className="flex-1 flex flex-col min-h-0 bg-white font-mono text-[6px]">
|
<div className="flex-1 flex flex-col min-h-0 bg-white font-mono text-[6px]">
|
||||||
{/* Headers Row */}
|
{/* Headers Row */}
|
||||||
@ -594,7 +592,7 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Excel Sheet Tab Footer */}
|
{/* Excel Sheet Tab Footer */}
|
||||||
<div className="h-4 bg-slate-50 border-t border-slate-200 flex items-center px-2 shrink-0 select-none justify-between">
|
<div className="h-4 bg-slate-50 border-t border-slate-200 flex items-center px-2 shrink-0 select-none justify-between">
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
@ -713,9 +711,9 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{asset.type === 'url' || asset.type === 'case_study' ? (
|
{asset.type === 'url' || asset.type === 'case_study' ? (
|
||||||
<a
|
<a
|
||||||
href={asset.url}
|
href={asset.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="w-7 h-7 rounded-md bg-ink-900 border border-ink-900 flex items-center justify-center text-ink-0 hover:bg-ink-800 transition-all shadow-sm"
|
className="w-7 h-7 rounded-md bg-ink-900 border border-ink-900 flex items-center justify-center text-ink-0 hover:bg-ink-800 transition-all shadow-sm"
|
||||||
title={asset.type === 'case_study' ? 'Open Case Study' : 'Open External URL'}
|
title={asset.type === 'case_study' ? 'Open Case Study' : 'Open External URL'}
|
||||||
@ -723,7 +721,7 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
<ExternalLink className="w-3.5 h-3.5" />
|
<ExternalLink className="w-3.5 h-3.5" />
|
||||||
</a>
|
</a>
|
||||||
) : canDirectDownload ? (
|
) : canDirectDownload ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => onDownload(asset)}
|
onClick={() => onDownload(asset)}
|
||||||
className="w-7 h-7 rounded-md bg-ink-50 border border-ink-200 flex items-center justify-center text-ink-600 hover:bg-ink-100 hover:border-ink-300 hover:text-ink-900 transition-all shadow-sm"
|
className="w-7 h-7 rounded-md bg-ink-50 border border-ink-200 flex items-center justify-center text-ink-600 hover:bg-ink-100 hover:border-ink-300 hover:text-ink-900 transition-all shadow-sm"
|
||||||
title="Download Asset"
|
title="Download Asset"
|
||||||
@ -736,7 +734,7 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
<span>Pending Access</span>
|
<span>Pending Access</span>
|
||||||
</div>
|
</div>
|
||||||
) : requestStatus === 'REJECTED' ? (
|
) : requestStatus === 'REJECTED' ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => onRequestDownload(asset)}
|
onClick={() => onRequestDownload(asset)}
|
||||||
className="inline-flex items-center gap-1 text-[10px] font-bold text-red-650 bg-red-500/10 border border-red-500/20 px-2 py-1 rounded-md hover:bg-red-500/25 transition-all"
|
className="inline-flex items-center gap-1 text-[10px] font-bold text-red-650 bg-red-500/10 border border-red-500/20 px-2 py-1 rounded-md hover:bg-red-500/25 transition-all"
|
||||||
>
|
>
|
||||||
@ -744,7 +742,7 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
<span>Rejected (Retry)</span>
|
<span>Rejected (Retry)</span>
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => onRequestDownload(asset)}
|
onClick={() => onRequestDownload(asset)}
|
||||||
className="inline-flex items-center gap-1 text-[10px] font-bold text-ink-900 bg-ink-100 border border-ink-300 hover:bg-ink-200 px-2 py-1 rounded-md transition-all cursor-pointer"
|
className="inline-flex items-center gap-1 text-[10px] font-bold text-ink-900 bg-ink-100 border border-ink-300 hover:bg-ink-200 px-2 py-1 rounded-md transition-all cursor-pointer"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -673,7 +673,7 @@ export const UploadAssetModal: React.FC<UploadAssetModalProps> = ({
|
|||||||
Include in AI Advisor Knowledge Base (RAG Search)
|
Include in AI Advisor Knowledge Base (RAG Search)
|
||||||
</label>
|
</label>
|
||||||
<span className="text-[10px] text-ink-500 dark:text-slate-400 block mt-0.5">
|
<span className="text-[10px] text-ink-500 dark:text-slate-400 block mt-0.5">
|
||||||
If checked, DeepSeek AI Advisor indexes this content to answer partner questions with OKF standard citations.
|
If checked, the AI Advisor indexes this content to answer partner questions with OKF standard citations.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -40,7 +40,7 @@
|
|||||||
--color-info: #3b82f6;
|
--color-info: #3b82f6;
|
||||||
|
|
||||||
/* ── Typography ── */
|
/* ── Typography ── */
|
||||||
--font-sans: 'Inter Variable', 'Inter', ui-sans-serif, system-ui, sans-serif;
|
--font-sans: 'Poppins', 'Inter', ui-sans-serif, system-ui, sans-serif;
|
||||||
--font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
|
--font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
|
||||||
|
|
||||||
/* ── Spacing Scale ── */
|
/* ── Spacing Scale ── */
|
||||||
|
|||||||
@ -597,8 +597,8 @@ export const AssetsPage = () => {
|
|||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate="show"
|
animate="show"
|
||||||
className={`grid grid-cols-1 ${viewMode === 'compact'
|
className={`grid grid-cols-1 ${viewMode === 'compact'
|
||||||
? 'md:grid-cols-3 xl:grid-cols-5 gap-3'
|
? 'sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-5 gap-3'
|
||||||
: 'md:grid-cols-2 xl:grid-cols-4 gap-4'
|
: 'sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 sm:gap-6'
|
||||||
} items-start`}
|
} items-start`}
|
||||||
>
|
>
|
||||||
{filteredAssets.map((asset) => (
|
{filteredAssets.map((asset) => (
|
||||||
|
|||||||
@ -80,7 +80,7 @@ export const ClientAgreementsPage: React.FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
{/* NDA Card */}
|
{/* NDA Card */}
|
||||||
<div
|
<div
|
||||||
id={`asset-card-${ndaAcceptance?.document?.id || 'nda'}`}
|
id={`asset-card-${ndaAcceptance?.document?.id || 'nda'}`}
|
||||||
draggable={true}
|
draggable={true}
|
||||||
onDragStart={(e) => {
|
onDragStart={(e) => {
|
||||||
@ -120,7 +120,7 @@ export const ClientAgreementsPage: React.FC = () => {
|
|||||||
title="Inspect NDA with AI Advisor Workbench"
|
title="Inspect NDA with AI Advisor Workbench"
|
||||||
>
|
>
|
||||||
<Sparkles className="w-3 h-3 text-amber-400 fill-amber-400" />
|
<Sparkles className="w-3 h-3 text-amber-400 fill-amber-400" />
|
||||||
<span>AI Workbench</span>
|
<span>ask AI</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{ndaAcceptance ? (
|
{ndaAcceptance ? (
|
||||||
@ -192,7 +192,7 @@ export const ClientAgreementsPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* MSA Card */}
|
{/* MSA Card */}
|
||||||
<div
|
<div
|
||||||
id={`asset-card-${msaAcceptance?.document?.id || 'msa'}`}
|
id={`asset-card-${msaAcceptance?.document?.id || 'msa'}`}
|
||||||
draggable={true}
|
draggable={true}
|
||||||
onDragStart={(e) => {
|
onDragStart={(e) => {
|
||||||
@ -232,7 +232,7 @@ export const ClientAgreementsPage: React.FC = () => {
|
|||||||
title="Inspect MSA with AI Advisor Workbench"
|
title="Inspect MSA with AI Advisor Workbench"
|
||||||
>
|
>
|
||||||
<Sparkles className="w-3 h-3 text-amber-400 fill-amber-400" />
|
<Sparkles className="w-3 h-3 text-amber-400 fill-amber-400" />
|
||||||
<span>AI Workbench</span>
|
<span>ask AI</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{msaAcceptance ? (
|
{msaAcceptance ? (
|
||||||
@ -377,7 +377,7 @@ export const ClientAgreementsPage: React.FC = () => {
|
|||||||
`Verification Hash: ${activeDocData.signatureHash || 'N/A'}\n`,
|
`Verification Hash: ${activeDocData.signatureHash || 'N/A'}\n`,
|
||||||
`IP Address: ${activeDocData.ipAddress}\n`,
|
`IP Address: ${activeDocData.ipAddress}\n`,
|
||||||
`Signed On: ${new Date(activeDocData.acceptedAt).toLocaleString()}\n`
|
`Signed On: ${new Date(activeDocData.acceptedAt).toLocaleString()}\n`
|
||||||
], {type: 'text/plain'});
|
], { type: 'text/plain' });
|
||||||
element.href = URL.createObjectURL(file);
|
element.href = URL.createObjectURL(file);
|
||||||
element.download = `${selectedDoc}_Agreement_${user?.email?.split('@')[0]}.txt`;
|
element.download = `${selectedDoc}_Agreement_${user?.email?.split('@')[0]}.txt`;
|
||||||
document.body.appendChild(element);
|
document.body.appendChild(element);
|
||||||
|
|||||||
@ -90,11 +90,10 @@ export const EcosystemPage: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
key={type}
|
key={type}
|
||||||
onClick={() => setFilter(type)}
|
onClick={() => setFilter(type)}
|
||||||
className={`px-4 py-2 rounded-lg text-xs font-black tracking-wider uppercase transition-all duration-300 whitespace-nowrap cursor-pointer ${
|
className={`px-4 py-2 rounded-lg text-xs font-black tracking-wider uppercase transition-all duration-300 whitespace-nowrap cursor-pointer ${filter === type
|
||||||
filter === type
|
|
||||||
? 'bg-ink-0 text-ink-900 shadow-sm border border-ink-200/50'
|
? 'bg-ink-0 text-ink-900 shadow-sm border border-ink-200/50'
|
||||||
: 'text-ink-500 hover:text-ink-900'
|
: 'text-ink-500 hover:text-ink-900'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{labels[type]}
|
{labels[type]}
|
||||||
</button>
|
</button>
|
||||||
@ -118,7 +117,7 @@ export const EcosystemPage: React.FC = () => {
|
|||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, y: -15 }}
|
exit={{ opacity: 0, y: -15 }}
|
||||||
transition={{ duration: 0.25 }}
|
transition={{ duration: 0.25 }}
|
||||||
className="grid grid-cols-1 md:grid-cols-2 gap-8"
|
className="grid grid-cols-1 lg:grid-cols-2 gap-6 lg:gap-8"
|
||||||
>
|
>
|
||||||
{filteredOfferings.map((offering) => {
|
{filteredOfferings.map((offering) => {
|
||||||
const IconComponent = iconMap[offering.logoIcon] || Globe;
|
const IconComponent = iconMap[offering.logoIcon] || Globe;
|
||||||
@ -176,14 +175,13 @@ export const EcosystemPage: React.FC = () => {
|
|||||||
title="Inspect Offering with AI Advisor Workbench"
|
title="Inspect Offering with AI Advisor Workbench"
|
||||||
>
|
>
|
||||||
<Sparkles className="w-3 h-3 text-amber-400 fill-amber-400" />
|
<Sparkles className="w-3 h-3 text-amber-400 fill-amber-400" />
|
||||||
<span>AI Workbench</span>
|
<span>ask AI</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[9px] font-black tracking-widest uppercase border ${
|
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[9px] font-black tracking-widest uppercase border ${isProduct
|
||||||
isProduct
|
|
||||||
? 'bg-blue-500/10 text-blue-600 border-blue-500/20'
|
? 'bg-blue-500/10 text-blue-600 border-blue-500/20'
|
||||||
: 'bg-emerald-500/10 text-emerald-600 border-emerald-500/20'
|
: 'bg-emerald-500/10 text-emerald-600 border-emerald-500/20'
|
||||||
}`}>
|
}`}>
|
||||||
<Layers className="w-3 h-3" />
|
<Layers className="w-3 h-3" />
|
||||||
{offering.type}
|
{offering.type}
|
||||||
</span>
|
</span>
|
||||||
@ -210,9 +208,8 @@ export const EcosystemPage: React.FC = () => {
|
|||||||
<ul className="grid grid-cols-1 gap-2.5 pt-1">
|
<ul className="grid grid-cols-1 gap-2.5 pt-1">
|
||||||
{offering.benefits.map((benefit, bIdx) => (
|
{offering.benefits.map((benefit, bIdx) => (
|
||||||
<li key={bIdx} className="flex items-start gap-2.5 text-xs font-medium text-ink-600">
|
<li key={bIdx} className="flex items-start gap-2.5 text-xs font-medium text-ink-600">
|
||||||
<span className={`w-1.5 h-1.5 rounded-full shrink-0 mt-1.5 ${
|
<span className={`w-1.5 h-1.5 rounded-full shrink-0 mt-1.5 ${isProduct ? 'bg-blue-500' : 'bg-emerald-500'
|
||||||
isProduct ? 'bg-blue-500' : 'bg-emerald-500'
|
}`} />
|
||||||
}`} />
|
|
||||||
<span>{benefit}</span>
|
<span>{benefit}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -129,9 +129,8 @@ const VideoDescription: React.FC<VideoDescriptionProps> = ({ text, isExpanded, o
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p
|
<p
|
||||||
className={`text-[11px] font-medium text-ink-500 leading-relaxed overflow-hidden ${
|
className={`text-[11px] font-medium text-ink-500 leading-relaxed overflow-hidden ${isExpanded ? '' : 'line-clamp-2'
|
||||||
isExpanded ? '' : 'line-clamp-2'
|
}`}
|
||||||
}`}
|
|
||||||
style={{ whiteSpace: isExpanded ? 'pre-wrap' : 'normal' }}
|
style={{ whiteSpace: isExpanded ? 'pre-wrap' : 'normal' }}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
@ -174,7 +173,7 @@ export const ShowcasePage: React.FC = () => {
|
|||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
}, [location.state, location.search, loading]);
|
}, [location.state, location.search, loading]);
|
||||||
|
|
||||||
// Resizable Lightbox state: compact | theater | cinema
|
// Resizable Lightbox state: compact | theater | cinema
|
||||||
const [lightboxSize, setLightboxSize] = useState<'compact' | 'theater' | 'cinema'>('compact');
|
const [lightboxSize, setLightboxSize] = useState<'compact' | 'theater' | 'cinema'>('compact');
|
||||||
|
|
||||||
@ -230,7 +229,7 @@ export const ShowcasePage: React.FC = () => {
|
|||||||
<p className="text-xs text-ink-500 mt-1 max-w-sm">There are no featured videos available in the showcase right now. Check back later!</p>
|
<p className="text-xs text-ink-500 mt-1 max-w-sm">There are no featured videos available in the showcase right now. Check back later!</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 animate-fadeIn">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 animate-fadeIn">
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
const ytId = extractYouTubeVideoId(item.youtubeUrl);
|
const ytId = extractYouTubeVideoId(item.youtubeUrl);
|
||||||
const isIg = item.youtubeUrl.includes('instagram.com');
|
const isIg = item.youtubeUrl.includes('instagram.com');
|
||||||
@ -257,11 +256,10 @@ export const ShowcasePage: React.FC = () => {
|
|||||||
e.dataTransfer.setData('text/plain', item.title);
|
e.dataTransfer.setData('text/plain', item.title);
|
||||||
}}
|
}}
|
||||||
transition={{ type: "spring", stiffness: 320, damping: 28 }}
|
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 ${
|
className={`group flex flex-col bg-ink-0 border rounded-2xl overflow-hidden transition-[border-color,box-shadow,background-color] duration-300 ${isExpanded
|
||||||
isExpanded
|
|
||||||
? 'absolute z-20 top-0 left-0 right-0 h-auto shadow-2xl border-ink-350 bg-ink-0'
|
? 'absolute z-20 top-0 left-0 right-0 h-auto shadow-2xl border-ink-350 bg-ink-0'
|
||||||
: 'relative w-full h-full border-ink-200 hover:border-ink-350 hover:shadow-xl'
|
: 'relative w-full h-full border-ink-200 hover:border-ink-350 hover:shadow-xl'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Video Player / Thumbnail */}
|
{/* Video Player / Thumbnail */}
|
||||||
<div className="relative aspect-video bg-ink-900 overflow-hidden shrink-0">
|
<div className="relative aspect-video bg-ink-900 overflow-hidden shrink-0">
|
||||||
@ -283,7 +281,7 @@ export const ShowcasePage: React.FC = () => {
|
|||||||
title="Inspect Reel with AI Advisor Workbench"
|
title="Inspect Reel with AI Advisor Workbench"
|
||||||
>
|
>
|
||||||
<Sparkles className="w-3 h-3 text-amber-400 fill-amber-400" />
|
<Sparkles className="w-3 h-3 text-amber-400 fill-amber-400" />
|
||||||
<span>AI Workbench</span>
|
<span>ask AI</span>
|
||||||
</button>
|
</button>
|
||||||
{thumbnail ? (
|
{thumbnail ? (
|
||||||
<img
|
<img
|
||||||
@ -293,13 +291,12 @@ export const ShowcasePage: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
/* Platform Fallback Gradients */
|
/* Platform Fallback Gradients */
|
||||||
<div className={`w-full h-full flex items-center justify-center ${
|
<div className={`w-full h-full flex items-center justify-center ${isIg
|
||||||
isIg
|
|
||||||
? 'bg-gradient-to-tr from-yellow-500 via-pink-500 to-purple-600'
|
? 'bg-gradient-to-tr from-yellow-500 via-pink-500 to-purple-600'
|
||||||
: isTw
|
: isTw
|
||||||
? 'bg-ink-950'
|
? 'bg-ink-950'
|
||||||
: 'bg-ink-100'
|
: 'bg-ink-100'
|
||||||
}`}>
|
}`}>
|
||||||
{isIg && (
|
{isIg && (
|
||||||
<svg className="w-12 h-12 text-white/90 animate-pulse" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
|
<svg className="w-12 h-12 text-white/90 animate-pulse" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
|
||||||
<rect x="2" y="2" width="20" height="20" rx="5" ry="5" />
|
<rect x="2" y="2" width="20" height="20" rx="5" ry="5" />
|
||||||
@ -344,7 +341,7 @@ export const ShowcasePage: React.FC = () => {
|
|||||||
{item.title}
|
{item.title}
|
||||||
</h3>
|
</h3>
|
||||||
{item.description && (
|
{item.description && (
|
||||||
<VideoDescription
|
<VideoDescription
|
||||||
text={item.description}
|
text={item.description}
|
||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
onToggleExpand={() => setExpandedItemId(isExpanded ? null : item.id)}
|
onToggleExpand={() => setExpandedItemId(isExpanded ? null : item.id)}
|
||||||
@ -390,13 +387,12 @@ export const ShowcasePage: React.FC = () => {
|
|||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||||
transition={{ type: 'spring', damping: 25, stiffness: 250 }}
|
transition={{ type: 'spring', damping: 25, stiffness: 250 }}
|
||||||
className={`relative bg-ink-900 border border-ink-800 rounded-3xl overflow-hidden shadow-2xl flex flex-col transition-all duration-300 ${
|
className={`relative bg-ink-900 border border-ink-800 rounded-3xl overflow-hidden shadow-2xl flex flex-col transition-all duration-300 ${lightboxSize === 'cinema'
|
||||||
lightboxSize === 'cinema'
|
? 'w-[95vw] max-w-7xl md:flex-col h-[85vh] md:h-[90vh]'
|
||||||
? 'w-[95vw] max-w-7xl md:flex-col h-[85vh] md:h-[90vh]'
|
: lightboxSize === 'theater'
|
||||||
: lightboxSize === 'theater'
|
? 'w-[95vw] md:w-[85vw] max-w-6xl md:flex-row h-[85vh] md:max-h-[85vh]'
|
||||||
? 'w-[95vw] md:w-[85vw] max-w-6xl md:flex-row h-[85vh] md:max-h-[85vh]'
|
: 'w-[95vw] md:w-full max-w-4xl md:flex-row h-[85vh] md:max-h-[80vh]'
|
||||||
: 'w-[95vw] md:w-full max-w-4xl md:flex-row h-[85vh] md:max-h-[80vh]'
|
}`}
|
||||||
}`}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{/* Media Container */}
|
{/* Media Container */}
|
||||||
@ -422,22 +418,20 @@ export const ShowcasePage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Info Container */}
|
{/* Info Container */}
|
||||||
<div className={`p-5 sm:p-6 flex flex-col justify-between border-ink-800 bg-ink-950 overflow-y-auto ${
|
<div className={`p-5 sm:p-6 flex flex-col justify-between border-ink-800 bg-ink-950 overflow-y-auto ${lightboxSize === 'cinema'
|
||||||
lightboxSize === 'cinema'
|
? 'w-full border-t h-[40%] md:h-[30%] shrink-0'
|
||||||
? 'w-full border-t h-[40%] md:h-[30%] shrink-0'
|
|
||||||
: 'w-full md:w-80 border-t md:border-t-0 md:border-l h-[45%] md:h-auto shrink-0'
|
: 'w-full md:w-80 border-t md:border-t-0 md:border-l h-[45%] md:h-auto shrink-0'
|
||||||
}`}>
|
}`}>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Toolbar with Resizer Preset Buttons */}
|
{/* Toolbar with Resizer Preset Buttons */}
|
||||||
<div className="flex justify-between items-center pb-2 border-b border-ink-800/60">
|
<div className="flex justify-between items-center pb-2 border-b border-ink-800/60">
|
||||||
<div className="hidden md:flex bg-ink-900 p-0.5 rounded-lg border border-ink-800 gap-0.5">
|
<div className="hidden md:flex bg-ink-900 p-0.5 rounded-lg border border-ink-800 gap-0.5">
|
||||||
<button
|
<button
|
||||||
onClick={() => setLightboxSize('compact')}
|
onClick={() => setLightboxSize('compact')}
|
||||||
className={`p-1.5 rounded text-[9px] font-black uppercase tracking-wider flex items-center gap-1 transition-all cursor-pointer ${
|
className={`p-1.5 rounded text-[9px] font-black uppercase tracking-wider flex items-center gap-1 transition-all cursor-pointer ${lightboxSize === 'compact'
|
||||||
lightboxSize === 'compact'
|
? 'bg-ink-800 text-ink-0'
|
||||||
? 'bg-ink-800 text-ink-0'
|
|
||||||
: 'text-ink-500 hover:text-ink-300'
|
: 'text-ink-500 hover:text-ink-300'
|
||||||
}`}
|
}`}
|
||||||
title="Compact View"
|
title="Compact View"
|
||||||
>
|
>
|
||||||
<Monitor className="w-3 h-3" />
|
<Monitor className="w-3 h-3" />
|
||||||
@ -445,11 +439,10 @@ export const ShowcasePage: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setLightboxSize('theater')}
|
onClick={() => setLightboxSize('theater')}
|
||||||
className={`p-1.5 rounded text-[9px] font-black uppercase tracking-wider flex items-center gap-1 transition-all cursor-pointer ${
|
className={`p-1.5 rounded text-[9px] font-black uppercase tracking-wider flex items-center gap-1 transition-all cursor-pointer ${lightboxSize === 'theater'
|
||||||
lightboxSize === 'theater'
|
? 'bg-ink-800 text-ink-0'
|
||||||
? 'bg-ink-800 text-ink-0'
|
|
||||||
: 'text-ink-500 hover:text-ink-300'
|
: 'text-ink-500 hover:text-ink-300'
|
||||||
}`}
|
}`}
|
||||||
title="Theater View"
|
title="Theater View"
|
||||||
>
|
>
|
||||||
<Tv className="w-3 h-3" />
|
<Tv className="w-3 h-3" />
|
||||||
@ -457,11 +450,10 @@ export const ShowcasePage: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setLightboxSize('cinema')}
|
onClick={() => setLightboxSize('cinema')}
|
||||||
className={`p-1.5 rounded text-[9px] font-black uppercase tracking-wider flex items-center gap-1 transition-all cursor-pointer ${
|
className={`p-1.5 rounded text-[9px] font-black uppercase tracking-wider flex items-center gap-1 transition-all cursor-pointer ${lightboxSize === 'cinema'
|
||||||
lightboxSize === 'cinema'
|
? 'bg-ink-800 text-ink-0'
|
||||||
? 'bg-ink-800 text-ink-0'
|
|
||||||
: 'text-ink-500 hover:text-ink-300'
|
: 'text-ink-500 hover:text-ink-300'
|
||||||
}`}
|
}`}
|
||||||
title="Cinema View"
|
title="Cinema View"
|
||||||
>
|
>
|
||||||
<Maximize2 className="w-3 h-3" />
|
<Maximize2 className="w-3 h-3" />
|
||||||
@ -490,7 +482,7 @@ export const ShowcasePage: React.FC = () => {
|
|||||||
<h3 className="text-base font-black tracking-tight text-ink-0 leading-snug">
|
<h3 className="text-base font-black tracking-tight text-ink-0 leading-snug">
|
||||||
{activeItem.title}
|
{activeItem.title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{activeItem.description && (
|
{activeItem.description && (
|
||||||
<div className="max-h-60 md:max-h-none overflow-y-auto pr-1">
|
<div className="max-h-60 md:max-h-none overflow-y-auto pr-1">
|
||||||
<p className="text-xs font-medium text-ink-400 leading-relaxed whitespace-pre-wrap">
|
<p className="text-xs font-medium text-ink-400 leading-relaxed whitespace-pre-wrap">
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export default defineConfig({
|
|||||||
plugins: [tailwindcss(), react()],
|
plugins: [tailwindcss(), react()],
|
||||||
server: {
|
server: {
|
||||||
allowedHosts: [
|
allowedHosts: [
|
||||||
"delegator-caregiver-overprice.ngrok-free.dev"
|
"hurricane-reverence-robin.ngrok-free.dev"
|
||||||
],
|
],
|
||||||
cors: true,
|
cors: true,
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|||||||
@ -34,4 +34,4 @@ fi
|
|||||||
|
|
||||||
# 3. Start Vite Dev Server
|
# 3. Start Vite Dev Server
|
||||||
info "Starting Vite frontend dev server..."
|
info "Starting Vite frontend dev server..."
|
||||||
npm run dev -- --host
|
node --max-old-space-size=512 node_modules/vite/bin/vite.js --host
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user