Chatbot_phase2_2407

This commit is contained in:
kenilkb 2026-07-24 17:05:44 +05:30
parent 4256143939
commit 4715e1b482
17 changed files with 658 additions and 557 deletions

5
.gitignore vendored
View File

@ -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

View File

@ -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())
: []; : [];

View File

@ -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);
});
}

View File

@ -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>

File diff suppressed because it is too large Load Diff

View File

@ -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>
)} )}

View File

@ -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)}
/> />
)} )}

View File

@ -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| :-");

View File

@ -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"
> >

View File

@ -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>

View File

@ -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 ── */

View File

@ -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) => (

View File

@ -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);

View File

@ -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>
))} ))}

View File

@ -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">

View File

@ -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: {

View File

@ -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