수정
This commit is contained in:
27
db_test.php
Normal file
27
db_test.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
$host = 'mysql';
|
||||
$db = 'chandj';
|
||||
$user = 'root';
|
||||
$pass = 'root';
|
||||
$charset = 'utf8mb4';
|
||||
|
||||
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
|
||||
try {
|
||||
$pdo = new PDO($dsn, $user, $pass, $options);
|
||||
echo "Connected successfully to database '$db' on host '$host'.";
|
||||
|
||||
// Optional: Try to list tables to be sure
|
||||
$stmt = $pdo->query("SHOW TABLES");
|
||||
$tables = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
echo "\nTables found: " . implode(", ", array_slice($tables, 0, 5)) . "...";
|
||||
|
||||
} catch (\PDOException $e) {
|
||||
echo "Connection failed: " . $e->getMessage();
|
||||
}
|
||||
?>
|
||||
24
ref/.gitignore
vendored
Normal file
24
ref/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
186
ref/App.tsx
Normal file
186
ref/App.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { SALES_ASSETS } from './constants';
|
||||
import { SalesAsset } from './types';
|
||||
import SalesCard from './components/SalesCard';
|
||||
import Assistant from './components/Assistant';
|
||||
import AssetDetailModal from './components/AssetDetailModal';
|
||||
import { LayoutGrid, Menu, Bell, Search, ShieldCheck, CheckCircle2 } from 'lucide-react';
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [activeFilter, setActiveFilter] = useState('All');
|
||||
const [selectedAsset, setSelectedAsset] = useState<SalesAsset | null>(null);
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
|
||||
// Filter Logic
|
||||
const filteredAssets = useMemo(() => {
|
||||
if (activeFilter === 'All') return SALES_ASSETS;
|
||||
|
||||
return SALES_ASSETS.filter(asset => {
|
||||
if (activeFilter === 'CEO Logic') return asset.tags.some(tag => ['Concept', 'Pitch', 'Pain Points', 'Solution', 'Closing'].includes(tag));
|
||||
if (activeFilter === 'Legal/Tax') return asset.tags.some(tag => ['Legal', 'Benefit', 'Risk', 'Finance'].includes(tag));
|
||||
if (activeFilter === 'Demo') return asset.tags.some(tag => ['Demo', 'Dashboard', 'Video', 'Mobile', 'UX', 'Infra'].includes(tag));
|
||||
return true;
|
||||
});
|
||||
}, [activeFilter]);
|
||||
|
||||
const handleDownload = () => {
|
||||
setShowToast(true);
|
||||
setTimeout(() => setShowToast(false), 3000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 text-slate-900 font-sans selection:bg-brand-200 selection:text-brand-900">
|
||||
|
||||
{/* Toast Notification */}
|
||||
<div
|
||||
className={`fixed top-24 right-4 z-[60] bg-slate-800 text-white px-6 py-4 rounded-xl shadow-2xl flex items-center gap-3 transition-all duration-500 transform ${showToast ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0'}`}
|
||||
>
|
||||
<CheckCircle2 className="text-green-400" size={24} />
|
||||
<div>
|
||||
<h4 className="font-bold text-sm">Download Started</h4>
|
||||
<p className="text-slate-400 text-xs">CodeBridgeX_Proposal_v2.4.pdf</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navbar */}
|
||||
<nav className="sticky top-0 z-30 bg-white/80 backdrop-blur-md border-b border-slate-200">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
<div className="flex items-center gap-3 cursor-pointer" onClick={() => setActiveFilter('All')}>
|
||||
<div className="w-8 h-8 bg-brand-600 rounded-lg flex items-center justify-center text-white font-bold text-lg shadow-lg shadow-brand-200">
|
||||
S
|
||||
</div>
|
||||
<span className="text-xl font-bold tracking-tight text-slate-900">CodeBridgeX <span className="text-brand-600">SAM</span></span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<button className="p-2 text-slate-500 hover:text-brand-600 transition-colors hidden sm:block">
|
||||
<Search size={20} />
|
||||
</button>
|
||||
<button className="p-2 text-slate-500 hover:text-brand-600 transition-colors relative">
|
||||
<Bell size={20} />
|
||||
<span className="absolute top-2 right-2 w-2 h-2 bg-red-500 rounded-full border border-white"></span>
|
||||
</button>
|
||||
<button className="p-2 text-slate-500 hover:text-slate-900 sm:hidden">
|
||||
<Menu size={24} />
|
||||
</button>
|
||||
<div className="hidden sm:flex items-center gap-2 ml-2">
|
||||
<div className="w-8 h-8 rounded-full bg-slate-200 flex items-center justify-center text-slate-500 font-bold border border-slate-300">
|
||||
S
|
||||
</div>
|
||||
<span className="text-sm font-medium text-slate-700">영업팀 관리자</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Hero Section */}
|
||||
<header className="relative bg-white pt-16 pb-20 lg:pt-24 lg:pb-28 overflow-hidden">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div className="text-center max-w-4xl mx-auto">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-brand-50 text-brand-700 text-xs font-semibold uppercase tracking-wider mb-6 animate-fade-in-up">
|
||||
<ShieldCheck size={14} />
|
||||
CEO Management Solution
|
||||
</div>
|
||||
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-extrabold text-slate-900 tracking-tight mb-6 animate-fade-in-up delay-100">
|
||||
직원의 관리 도구가 아닙니다.<br />
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-brand-600 to-indigo-600">
|
||||
대표님의 경영 무기입니다.
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-lg sm:text-xl text-slate-500 mb-10 leading-relaxed max-w-2xl mx-auto animate-fade-in-up delay-200">
|
||||
"SAM"은 단순한 ERP가 아닙니다. <br className="hidden sm:block"/>
|
||||
가지급금 이자 계산부터 채권 추심, 실시간 경영 알림까지.<br />
|
||||
<strong>오직 CEO를 위한 시크릿 대시보드</strong>를 제안하십시오.
|
||||
</p>
|
||||
<div className="flex justify-center gap-4 animate-fade-in-up delay-300">
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="px-8 py-3 rounded-xl bg-slate-900 text-white font-semibold hover:bg-slate-800 hover:shadow-lg transition-all transform hover:-translate-y-1 active:scale-95"
|
||||
>
|
||||
제안서 다운로드
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveFilter('Demo')}
|
||||
className="px-8 py-3 rounded-xl bg-white text-slate-700 border border-slate-200 font-semibold hover:bg-slate-50 hover:border-slate-300 transition-all flex items-center gap-2 active:scale-95"
|
||||
>
|
||||
<LayoutGrid size={18} />
|
||||
세일즈 덱 보기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Background Decorations */}
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-full h-full z-0 pointer-events-none">
|
||||
<div className="absolute top-20 left-10 w-72 h-72 bg-brand-200 rounded-full mix-blend-multiply filter blur-3xl opacity-30 animate-blob"></div>
|
||||
<div className="absolute top-20 right-10 w-72 h-72 bg-indigo-200 rounded-full mix-blend-multiply filter blur-3xl opacity-30 animate-blob animation-delay-2000"></div>
|
||||
<div className="absolute -bottom-8 left-1/2 w-72 h-72 bg-pink-200 rounded-full mix-blend-multiply filter blur-3xl opacity-30 animate-blob animation-delay-4000"></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Grid Content */}
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h2 className="text-2xl font-bold text-slate-900">Sales Materials</h2>
|
||||
<div className="flex gap-2">
|
||||
{['All', 'CEO Logic', 'Legal/Tax', 'Demo'].map((filter) => (
|
||||
<button
|
||||
key={filter}
|
||||
onClick={() => setActiveFilter(filter)}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors duration-200 ${
|
||||
activeFilter === filter
|
||||
? 'bg-slate-900 text-white shadow-md'
|
||||
: 'bg-white text-slate-600 hover:bg-slate-100 border border-transparent hover:border-slate-200'
|
||||
}`}
|
||||
>
|
||||
{filter}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Masonry-like Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 auto-rows-[minmax(200px,auto)]">
|
||||
{filteredAssets.map((asset) => (
|
||||
<SalesCard
|
||||
key={asset.id}
|
||||
asset={asset}
|
||||
onClick={(a) => setSelectedAsset(a)}
|
||||
/>
|
||||
))}
|
||||
{filteredAssets.length === 0 && (
|
||||
<div className="col-span-3 text-center py-20 text-slate-400">
|
||||
해당 카테고리에 자료가 없습니다.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-white border-t border-slate-200 py-12 mt-12">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col md:flex-row justify-between items-center gap-6">
|
||||
<p className="text-slate-500 text-sm">© 2024 CodeBridgeX Corp. Sales Enablement Portal.</p>
|
||||
<div className="flex gap-6">
|
||||
<a href="#" className="text-slate-400 hover:text-slate-600">Internal Use Only</a>
|
||||
<a href="#" className="text-slate-400 hover:text-slate-600">Sales Script</a>
|
||||
<a href="#" className="text-slate-400 hover:text-slate-600">Support</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{/* Detail Modal */}
|
||||
{selectedAsset && (
|
||||
<AssetDetailModal
|
||||
asset={selectedAsset}
|
||||
onClose={() => setSelectedAsset(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* AI Assistant */}
|
||||
<Assistant />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
20
ref/README.md
Normal file
20
ref/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
<div align="center">
|
||||
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
||||
</div>
|
||||
|
||||
# Run and deploy your AI Studio app
|
||||
|
||||
This contains everything you need to run your app locally.
|
||||
|
||||
View your app in AI Studio: https://ai.studio/apps/drive/1yue3NOVr2MDV0U4mHpOBv1pf3ByPhT2-
|
||||
|
||||
## Run Locally
|
||||
|
||||
**Prerequisites:** Node.js
|
||||
|
||||
|
||||
1. Install dependencies:
|
||||
`npm install`
|
||||
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||
3. Run the app:
|
||||
`npm run dev`
|
||||
173
ref/components/AssetDetailModal.tsx
Normal file
173
ref/components/AssetDetailModal.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { SalesAsset, AssetType } from '../types';
|
||||
import { X, Copy, Check, MessageSquare, Play, Maximize2 } from 'lucide-react';
|
||||
|
||||
interface AssetDetailModalProps {
|
||||
asset: SalesAsset | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const AssetDetailModal: React.FC<AssetDetailModalProps> = ({ asset, onClose }) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleEsc = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
window.addEventListener('keydown', handleEsc);
|
||||
return () => window.removeEventListener('keydown', handleEsc);
|
||||
}, [onClose]);
|
||||
|
||||
if (!asset) return null;
|
||||
|
||||
const handleCopyScript = () => {
|
||||
if (asset.script) {
|
||||
navigator.clipboard.writeText(asset.script);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6">
|
||||
<div
|
||||
className="absolute inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
<div className="relative w-full max-w-4xl max-h-[90vh] bg-white rounded-2xl shadow-2xl flex flex-col overflow-hidden animate-in fade-in zoom-in-95 duration-200">
|
||||
|
||||
{/* Header (Absolute for Video/Image types to float over, Relative for others) */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-4 right-4 z-20 p-2 bg-black/20 hover:bg-black/40 text-white rounded-full transition-colors backdrop-blur-md"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar">
|
||||
{/* Media Section */}
|
||||
<div className="relative bg-slate-100 w-full min-h-[300px] md:min-h-[400px] flex items-center justify-center group">
|
||||
{asset.type === AssetType.VIDEO && (
|
||||
<iframe
|
||||
src={`https://player.vimeo.com/video/${asset.src}?autoplay=1&title=0&byline=0&portrait=0`}
|
||||
className="w-full h-full absolute inset-0"
|
||||
frameBorder="0"
|
||||
allow="autoplay; fullscreen; picture-in-picture"
|
||||
allowFullScreen
|
||||
title={asset.videoTitle}
|
||||
></iframe>
|
||||
)}
|
||||
{asset.type === AssetType.IMAGE && (
|
||||
<img
|
||||
src={asset.src}
|
||||
alt={asset.title}
|
||||
className="w-full h-full object-contain bg-slate-900"
|
||||
/>
|
||||
)}
|
||||
{asset.type === AssetType.STAT && (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center bg-brand-600 text-white p-12">
|
||||
<div className="text-8xl font-extrabold tracking-tighter mb-4">{asset.statValue}</div>
|
||||
<div className="text-2xl text-brand-100 font-medium">{asset.statLabel}</div>
|
||||
</div>
|
||||
)}
|
||||
{asset.type === AssetType.TEXT && (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center bg-gradient-to-br from-slate-800 to-slate-900 text-white p-12 text-center">
|
||||
<MessageSquare size={64} className="mb-6 opacity-50" />
|
||||
<h2 className="text-3xl md:text-4xl font-bold max-w-2xl leading-tight">{asset.title}</h2>
|
||||
<div className="mt-4 flex gap-2">
|
||||
{asset.tags.map(tag => (
|
||||
<span key={tag} className="text-xs font-semibold bg-white/10 px-3 py-1 rounded-full">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content Section */}
|
||||
<div className="p-6 md:p-8">
|
||||
<div className="flex flex-col md:flex-row gap-8">
|
||||
|
||||
{/* Left: Info */}
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-bold text-slate-900 mb-2 flex items-center gap-2">
|
||||
{asset.title}
|
||||
{asset.type !== AssetType.TEXT && (
|
||||
<div className="flex gap-2">
|
||||
{asset.tags.map(tag => (
|
||||
<span key={tag} className="text-[10px] uppercase font-bold text-brand-600 bg-brand-50 px-2 py-1 rounded-md border border-brand-100">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</h2>
|
||||
<p className="text-slate-600 text-lg leading-relaxed mb-6">
|
||||
{asset.content || asset.description}
|
||||
</p>
|
||||
|
||||
{/* Additional Metadata if needed */}
|
||||
<div className="grid grid-cols-2 gap-4 text-sm text-slate-500 bg-slate-50 p-4 rounded-xl">
|
||||
<div>
|
||||
<span className="block font-semibold text-slate-900 mb-1">Asset Type</span>
|
||||
{asset.type}
|
||||
</div>
|
||||
<div>
|
||||
<span className="block font-semibold text-slate-900 mb-1">Last Updated</span>
|
||||
2024.05.20
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Sales Script (The "Key" feature) */}
|
||||
<div className="w-full md:w-96 bg-brand-50 rounded-xl p-6 border border-brand-100 flex flex-col">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="font-bold text-brand-900 flex items-center gap-2">
|
||||
<MessageSquare size={18} />
|
||||
Sales Pitch Script
|
||||
</h3>
|
||||
<button
|
||||
onClick={handleCopyScript}
|
||||
className="text-xs flex items-center gap-1 text-brand-600 hover:text-brand-800 font-medium transition-colors"
|
||||
>
|
||||
{copied ? <Check size={14} /> : <Copy size={14} />}
|
||||
{copied ? 'Copied!' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 bg-white p-4 rounded-lg text-slate-700 text-sm leading-relaxed shadow-sm border border-brand-100 italic relative">
|
||||
<span className="absolute top-2 left-2 text-4xl text-brand-100 font-serif leading-none">“</span>
|
||||
<div className="relative z-10 pt-2 pb-2 px-1">
|
||||
{asset.script ? asset.script : "No specific script available for this asset. Use the description as a guide."}
|
||||
</div>
|
||||
<span className="absolute bottom-[-10px] right-4 text-4xl text-brand-100 font-serif leading-none rotate-180">“</span>
|
||||
</div>
|
||||
<div className="mt-4 pt-4 border-t border-brand-200">
|
||||
<p className="text-xs text-brand-600 text-center font-medium">
|
||||
Tip: 고객의 눈을 맞추고 천천히 강조하며 읽으세요.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Actions */}
|
||||
<div className="p-4 border-t border-slate-100 bg-slate-50 flex justify-end gap-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-5 py-2.5 rounded-xl border border-slate-300 text-slate-700 font-medium hover:bg-slate-100 transition-colors"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button className="px-5 py-2.5 rounded-xl bg-slate-900 text-white font-medium hover:bg-slate-800 transition-colors shadow-lg flex items-center gap-2">
|
||||
Share Asset Link <Maximize2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssetDetailModal;
|
||||
160
ref/components/Assistant.tsx
Normal file
160
ref/components/Assistant.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { GoogleGenAI } from "@google/genai";
|
||||
import { Message } from '../types';
|
||||
import { SYSTEM_INSTRUCTION } from '../constants';
|
||||
import { MessageSquare, X, Send, Loader2, Sparkles } from 'lucide-react';
|
||||
|
||||
const Assistant: React.FC = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [messages, setMessages] = useState<Message[]>([
|
||||
{ role: 'model', text: '안녕하십니까! CodeBridgeX 영업 지원 AI입니다. CEO 설득을 위한 핵심 스크립트나 자료가 필요하신가요?' }
|
||||
]);
|
||||
const [input, setInput] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Auto-scroll to bottom
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [messages, isOpen]);
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!input.trim() || isLoading) return;
|
||||
|
||||
const userMessage = input.trim();
|
||||
setInput('');
|
||||
setMessages(prev => [...prev, { role: 'user', text: userMessage }]);
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const apiKey = process.env.API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("API Key is missing");
|
||||
}
|
||||
|
||||
const ai = new GoogleGenAI({ apiKey });
|
||||
|
||||
// Build history for context
|
||||
const history = messages.map(m => ({
|
||||
role: m.role,
|
||||
parts: [{ text: m.text }]
|
||||
}));
|
||||
|
||||
const chat = ai.chats.create({
|
||||
model: 'gemini-2.5-flash',
|
||||
config: {
|
||||
systemInstruction: SYSTEM_INSTRUCTION,
|
||||
},
|
||||
history: history
|
||||
});
|
||||
|
||||
const result = await chat.sendMessageStream({ message: userMessage });
|
||||
|
||||
let fullResponse = '';
|
||||
setMessages(prev => [...prev, { role: 'model', text: '' }]); // Placeholder
|
||||
|
||||
for await (const chunk of result) {
|
||||
const text = chunk.text;
|
||||
if (text) {
|
||||
fullResponse += text;
|
||||
setMessages(prev => {
|
||||
const newMsgs = [...prev];
|
||||
newMsgs[newMsgs.length - 1].text = fullResponse;
|
||||
return newMsgs;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("AI Error:", error);
|
||||
setMessages(prev => [...prev, { role: 'model', text: '죄송합니다. 일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' }]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Floating Action Button */}
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={`fixed bottom-6 right-6 z-50 p-4 rounded-full shadow-2xl transition-all duration-300 hover:scale-105 ${
|
||||
isOpen ? 'bg-slate-800 text-white rotate-90' : 'bg-brand-600 text-white'
|
||||
}`}
|
||||
>
|
||||
{isOpen ? <X size={24} /> : <MessageSquare size={24} />}
|
||||
</button>
|
||||
|
||||
{/* Chat Window */}
|
||||
<div
|
||||
className={`fixed bottom-24 right-6 z-40 w-[90vw] md:w-[380px] bg-white rounded-2xl shadow-2xl border border-slate-200 overflow-hidden flex flex-col transition-all duration-300 origin-bottom-right transform ${
|
||||
isOpen ? 'scale-100 opacity-100 translate-y-0' : 'scale-90 opacity-0 translate-y-10 pointer-events-none'
|
||||
}`}
|
||||
style={{ height: '600px', maxHeight: '75vh' }}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="bg-slate-900 p-4 flex items-center gap-3">
|
||||
<div className="bg-brand-500 p-2 rounded-lg">
|
||||
<Sparkles size={18} className="text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-white font-bold text-sm">SAM Sales Assistant</h3>
|
||||
<p className="text-slate-400 text-xs">Powered by Gemini 2.5 Flash</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto p-4 bg-slate-50 space-y-4">
|
||||
{messages.map((msg, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[85%] p-3 rounded-2xl text-sm leading-relaxed ${
|
||||
msg.role === 'user'
|
||||
? 'bg-brand-600 text-white rounded-tr-none'
|
||||
: 'bg-white text-slate-800 border border-slate-200 rounded-tl-none shadow-sm'
|
||||
}`}
|
||||
>
|
||||
{msg.text}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{isLoading && (
|
||||
<div className="flex justify-start">
|
||||
<div className="bg-white p-3 rounded-2xl rounded-tl-none border border-slate-200 shadow-sm">
|
||||
<Loader2 size={16} className="animate-spin text-brand-500" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input Area */}
|
||||
<div className="p-3 bg-white border-t border-slate-100">
|
||||
<div className="flex items-center gap-2 bg-slate-100 rounded-full px-4 py-2 border border-transparent focus-within:border-brand-300 focus-within:bg-white transition-colors">
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
|
||||
placeholder="제안 스크립트를 요청하세요..."
|
||||
className="flex-1 bg-transparent outline-none text-sm text-slate-800 placeholder:text-slate-400"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={isLoading || !input.trim()}
|
||||
className="text-brand-600 hover:text-brand-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Send size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Assistant;
|
||||
124
ref/components/SalesCard.tsx
Normal file
124
ref/components/SalesCard.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import React from 'react';
|
||||
import { SalesAsset, AssetType } from '../types';
|
||||
import { Play, ArrowUpRight, BarChart3, Image as ImageIcon } from 'lucide-react';
|
||||
|
||||
interface SalesCardProps {
|
||||
asset: SalesAsset;
|
||||
onClick: (asset: SalesAsset) => void;
|
||||
}
|
||||
|
||||
const SalesCard: React.FC<SalesCardProps> = ({ asset, onClick }) => {
|
||||
const baseClasses = `
|
||||
group relative overflow-hidden rounded-2xl bg-white border border-slate-100 shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-300 ease-out cursor-pointer
|
||||
${asset.gridSpan === 'col-span-2' ? 'md:col-span-2' : 'md:col-span-1'}
|
||||
${asset.rowSpan === 'row-span-2' ? 'md:row-span-2' : 'md:row-span-1'}
|
||||
flex flex-col
|
||||
`;
|
||||
|
||||
// Handle click properly
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
// Prevent triggering if clicking specific action buttons if needed,
|
||||
// but generally the whole card should open the modal.
|
||||
onClick(asset);
|
||||
};
|
||||
|
||||
// Render Video Card
|
||||
if (asset.type === AssetType.VIDEO) {
|
||||
return (
|
||||
<div className={`${baseClasses} bg-slate-900 text-white min-h-[300px]`} onClick={handleClick}>
|
||||
<div className="absolute inset-0 w-full h-full pointer-events-none">
|
||||
{/* Pointer events none on iframe to allow card click, or use a cover div */}
|
||||
<iframe
|
||||
src={`https://player.vimeo.com/video/${asset.src}?background=1&autoplay=0&loop=0&byline=0&title=0`}
|
||||
className="w-full h-full object-cover opacity-60 group-hover:opacity-40 transition-opacity duration-500"
|
||||
frameBorder="0"
|
||||
allow="autoplay; fullscreen; picture-in-picture"
|
||||
title={asset.videoTitle}
|
||||
></iframe>
|
||||
</div>
|
||||
<div className="relative z-10 p-6 flex flex-col h-full justify-between pointer-events-none">
|
||||
<div className="flex justify-between items-start">
|
||||
<span className="bg-red-600 text-white text-xs font-bold px-2 py-1 rounded-full uppercase tracking-wider flex items-center gap-1">
|
||||
<Play size={10} fill="currentColor" /> Video
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold mb-2 leading-tight">{asset.title}</h3>
|
||||
<p className="text-slate-300 text-sm line-clamp-2">{asset.description}</p>
|
||||
</div>
|
||||
<div className="mt-4 inline-flex items-center gap-2 text-sm font-medium text-white/80 group-hover:text-red-400 transition-colors">
|
||||
상세 보기 및 스크립트 <ArrowUpRight size={16} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Render Image Card
|
||||
if (asset.type === AssetType.IMAGE) {
|
||||
return (
|
||||
<div className={`${baseClasses} min-h-[250px]`} onClick={handleClick}>
|
||||
<div className="absolute inset-0">
|
||||
<img
|
||||
src={asset.src}
|
||||
alt={asset.title}
|
||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-80" />
|
||||
</div>
|
||||
<div className="relative z-10 p-6 mt-auto text-white h-full flex flex-col justify-end">
|
||||
<div className="mb-auto">
|
||||
<span className="bg-white/20 backdrop-blur-md text-white text-xs font-bold px-2 py-1 rounded-full uppercase tracking-wider inline-flex items-center gap-1">
|
||||
<ImageIcon size={10} /> Image
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-1">{asset.title}</h3>
|
||||
<p className="text-slate-200 text-sm">{asset.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Render Stat Card
|
||||
if (asset.type === AssetType.STAT) {
|
||||
return (
|
||||
<div className={`${baseClasses} bg-brand-600 text-white p-6 flex flex-col justify-between min-h-[200px]`} onClick={handleClick}>
|
||||
<div className="flex justify-between items-start opacity-80">
|
||||
<BarChart3 size={24} />
|
||||
<span className="text-xs font-medium bg-white/10 px-2 py-1 rounded-lg">{asset.tags[0]}</span>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="text-5xl font-extrabold tracking-tight mb-1">{asset.statValue}</div>
|
||||
<div className="text-brand-100 font-medium">{asset.statLabel}</div>
|
||||
</div>
|
||||
<div className="mt-4 text-xs text-brand-100 opacity-60 flex items-center justify-between">
|
||||
<span>* 내부 데이터 기준</span>
|
||||
<ArrowUpRight size={16} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Render Text Card (Default)
|
||||
return (
|
||||
<div className={`${baseClasses} p-6 flex flex-col justify-between min-h-[200px] hover:border-brand-200`} onClick={handleClick}>
|
||||
<div>
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{asset.tags.map(tag => (
|
||||
<span key={tag} className="text-[10px] uppercase font-bold text-slate-500 bg-slate-100 px-2 py-1 rounded-md">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-slate-900 mb-3">{asset.title}</h3>
|
||||
<p className="text-slate-600 text-sm leading-relaxed line-clamp-4">{asset.content}</p>
|
||||
</div>
|
||||
<div className="mt-6 pt-4 border-t border-slate-100 flex items-center justify-between">
|
||||
<span className="text-xs text-slate-400 font-medium group-hover:text-brand-600 transition-colors">전체 스크립트 보기</span>
|
||||
<ArrowUpRight size={16} className="text-slate-400 group-hover:text-brand-600 transition-colors" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SalesCard;
|
||||
119
ref/constants.ts
Normal file
119
ref/constants.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { SalesAsset, AssetType } from './types';
|
||||
|
||||
export const SYSTEM_INSTRUCTION = `
|
||||
You are "SAM AI", an expert sales assistant for CodeBridgeX's SAM (Smart Automation Management) solution.
|
||||
Your target audience is Sales Managers pitching to SMB CEOs.
|
||||
|
||||
Key Value Proposition:
|
||||
"SAM is not just an ERP for staff; it is a Weapon for the CEO."
|
||||
"ERP is a tool for employees to report; SAM is a dashboard for the CEO to control."
|
||||
|
||||
Core Features to Highlight:
|
||||
1. **CEO-Centric View:** Real-time dashboard replacing manual reports.
|
||||
2. **Financial Intelligence:** Real-time calculation of suspense payment interest (4.6%), estimated VAT, and entertainment expense limits.
|
||||
3. **Legal Support:** Built-in debt collection management with a dedicated lawyer (0 won retainer, 20% success fee).
|
||||
4. **Emotional UX:** "SAM~" notification sound for new orders/payments to boost morale.
|
||||
5. **Infrastructure:** Unlimited IP access (mobile included) and free GPS-based attendance tracking.
|
||||
|
||||
When answering, guide the sales rep on how to overcome objections using these points. Emphasize that this is a "Management Instrument," not just software.
|
||||
`;
|
||||
|
||||
export const SALES_ASSETS: SalesAsset[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: AssetType.TEXT,
|
||||
title: 'Concept: 대표를 위한 무기',
|
||||
content: '기존 ERP는 직원의 관리 도구였지만, SAM은 대표님의 의사결정 무기입니다. 직원의 보고를 기다리지 마십시오. SAM이 대표님께 직접, 실시간으로 회사의 현황을 보고합니다.',
|
||||
script: "대표님, ERP나 MES 들어보셨죠? 보통 직원들이 입력하고 관리하는 도구입니다. 정작 대표님은 직원한테 보고를 받아야만 회사를 알 수 있죠. SAM은 반대입니다. 직원이 아니라 '대표님을 위한 무기'입니다. 외근 중이든 집이든, 대표님 폰에서 회사의 자금, 인력, 리스크가 한눈에 보입니다.",
|
||||
tags: ['Concept', 'Pitch', 'Opener'],
|
||||
gridSpan: 'col-span-2',
|
||||
rowSpan: 'row-span-1'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: AssetType.STAT,
|
||||
title: '법무 지원 비용',
|
||||
statValue: '0원',
|
||||
statLabel: '변호사 착수금',
|
||||
script: "미수금 문제로 변호사 찾으시면 기본 착수금만 300~500만원 달라고 하죠? 배보다 배꼽이 더 큽니다. SAM을 쓰시면 '착수금 0원'에 전담 법무팀이 생깁니다. 성공보수만 20% 주시면 됩니다. 못 받으면 비용도 없습니다.",
|
||||
tags: ['Legal', 'Benefit'],
|
||||
gridSpan: 'col-span-1',
|
||||
rowSpan: 'row-span-1'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: AssetType.VIDEO,
|
||||
title: 'CEO 시크릿 대시보드',
|
||||
src: '76979871', // Placeholder for Dashboard Demo
|
||||
videoTitle: 'CEO Dashboard Demo',
|
||||
description: '가지급금 이자 4.6%, 예상 부가세, 미수금 현황이 오직 대표님 화면에만 실시간으로 계산되어 표시됩니다.',
|
||||
script: "이 화면은 직원들은 못 봅니다. 오직 대표님 아이디로 로그인했을 때만 뜹니다. 여기 붉은 글씨 보이시죠? 현재 가지급금에 대한 인정이자 4.6%가 실시간으로 계산돼서 '대표님, 세금 폭탄 조심하세요'라고 경고해주는 겁니다. 세무사가 알려주기 전에 SAM이 먼저 알려드립니다.",
|
||||
tags: ['Demo', 'Dashboard', 'Finance'],
|
||||
gridSpan: 'col-span-2',
|
||||
rowSpan: 'row-span-2'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: AssetType.TEXT,
|
||||
title: '20가지 고충 해결',
|
||||
content: '사람(근태, 인수인계), 돈(세금, 미수금), 운영(현장관리), 대표의 삶(리스크). CEO가 겪는 20가지 핵심 고충을 시스템 하나로 방어합니다.',
|
||||
script: "중소기업 대표님의 머릿속을 20가지로 정리해봤습니다. 직원 근태, 자금 압박, 세무 조사... 이 모든 걸 혼자 감당하고 계시지 않습니까? SAM은 단순 프로그램이 아니라, 이 20가지 리스크를 막아주는 방패입니다.",
|
||||
tags: ['Pain Points', 'Solution'],
|
||||
gridSpan: 'col-span-1',
|
||||
rowSpan: 'row-span-1'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
type: AssetType.IMAGE,
|
||||
title: '모바일 & 감성 알림',
|
||||
src: 'https://images.unsplash.com/photo-1556742049-0cfed4f7a07d?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
|
||||
description: '수주/입금 시 울리는 "SAM~" 알림음. 외근 중에도 회사가 돌아가는 소리를 들으세요.',
|
||||
script: "가장 인기 있는 기능입니다. 외근 나가 계실 때 불안하시죠? 직원이 큰 수주를 따오거나, 거래처에서 돈을 입금하면 대표님 폰에서 'SAM~' 하고 알림이 옵니다. 그 소리만 들으면 '아, 우리 회사 잘 돌아가고 있구나' 안심이 되실 겁니다.",
|
||||
tags: ['Mobile', 'UX', 'Emotion'],
|
||||
gridSpan: 'col-span-1',
|
||||
rowSpan: 'row-span-1'
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
type: AssetType.STAT,
|
||||
title: '인프라 혁신',
|
||||
statValue: '무제한',
|
||||
statLabel: '접속 IP 및 모바일',
|
||||
script: "타사 ERP는 아이디 하나 추가할 때마다 돈 받죠? 접속 IP도 3개로 제한하고요. 저희는 통 큽니다. 직원 100명이 써도, 집에서 쓰든 카페에서 쓰든 '무제한'입니다. 추가 비용 걱정 없이 맘껏 쓰십시오.",
|
||||
tags: ['Infra', 'GPS', 'Attendance'],
|
||||
gridSpan: 'col-span-1',
|
||||
rowSpan: 'row-span-1'
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
type: AssetType.TEXT,
|
||||
title: '자동화 & 인수인계',
|
||||
content: '직원이 갑자기 퇴사해도 걱정 없습니다. 견적부터 발주, 출고까지 모든 이력이 클릭 한 번으로 자동 인수인계됩니다. USB를 찾을 필요가 없습니다.',
|
||||
script: "직원이 갑자기 그만둔다고 하면 눈앞이 캄캄하시죠? 파일 어디 있냐, 거래처 연락처 뭐냐... SAM을 쓰시면 그럴 일 없습니다. 모든 업무 기록이 서버에 남기 때문에, 후임자는 '클릭' 한 번이면 전임자의 모든 업무를 그대로 이어받습니다.",
|
||||
tags: ['Automation', 'Management'],
|
||||
gridSpan: 'col-span-1',
|
||||
rowSpan: 'row-span-1'
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
type: AssetType.VIDEO,
|
||||
title: '채권 추심 & 법무',
|
||||
src: '824804225', // Placeholder for Legal Feature
|
||||
videoTitle: 'Legal Support System',
|
||||
description: '미수금 소송 포기하지 마세요. SAM 도입 시 전담 법무팀/심사팀이 배정됩니다. 성공보수 20%, 법원 출석 대행까지.',
|
||||
script: "거래처가 돈 안 주고 버티면 대표님이 직접 전화해서 싫은 소리 하셔야 하죠? 이제 SAM 버튼만 누르세요. 내용증명 발송부터 가압류, 소송까지 저희 변호사가 알아서 처리하고 진행 상황을 앱으로 보고합니다. 감정 소모는 저희가 하겠습니다.",
|
||||
tags: ['Legal', 'Risk'],
|
||||
gridSpan: 'col-span-2',
|
||||
rowSpan: 'row-span-1'
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
type: AssetType.TEXT,
|
||||
title: '도입 제안 (Closing)',
|
||||
content: '월 구독료로 수천만 원대 맞춤형 ERP 기능과 개인 비서, 전담 변호사를 고용하는 효과를 누리십시오. 내일부터는 "걱정" 대신 "설렘"으로 출근하십시오.',
|
||||
script: "직원 한 명 월급의 1/10도 안 되는 비용입니다. 이 돈으로 24시간 비서, 전담 변호사, 그리고 완벽한 경영 시스템을 고용하시는 겁니다. 오늘 결정하시고, 내일부터는 가벼운 마음으로 출근하십시오.",
|
||||
tags: ['Closing', 'Pricing'],
|
||||
gridSpan: 'col-span-1',
|
||||
rowSpan: 'row-span-1'
|
||||
},
|
||||
];
|
||||
63
ref/index.html
Normal file
63
ref/index.html
Normal file
@@ -0,0 +1,63 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>SAM - Sales Asset Manager</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
},
|
||||
colors: {
|
||||
brand: {
|
||||
50: '#f0f9ff',
|
||||
100: '#e0f2fe',
|
||||
500: '#0ea5e9',
|
||||
600: '#0284c7',
|
||||
900: '#0c4a6e',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
/* Custom Scrollbar for a sleek look */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
</style>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"react/": "https://esm.sh/react@^19.2.3/",
|
||||
"react": "https://esm.sh/react@^19.2.3",
|
||||
"react-dom/": "https://esm.sh/react-dom@^19.2.3/",
|
||||
"@google/genai": "https://esm.sh/@google/genai@^1.33.0",
|
||||
"lucide-react": "https://esm.sh/lucide-react@^0.561.0"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link rel="stylesheet" href="/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
15
ref/index.tsx
Normal file
15
ref/index.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
if (!rootElement) {
|
||||
throw new Error("Could not find root element to mount to");
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
5
ref/metadata.json
Normal file
5
ref/metadata.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "SAM - CodeBridgeX Sales",
|
||||
"description": "Sales enablement platform for CodeBridgeX SAM - The ERP for CEOs.",
|
||||
"requestFramePermissions": []
|
||||
}
|
||||
23
ref/package.json
Normal file
23
ref/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "sam---codebridgex-sales",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"@google/genai": "^1.33.0",
|
||||
"lucide-react": "^0.561.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
29
ref/tsconfig.json
Normal file
29
ref/tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
27
ref/types.ts
Normal file
27
ref/types.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export enum AssetType {
|
||||
IMAGE = 'IMAGE',
|
||||
VIDEO = 'VIDEO',
|
||||
TEXT = 'TEXT',
|
||||
STAT = 'STAT'
|
||||
}
|
||||
|
||||
export interface SalesAsset {
|
||||
id: string;
|
||||
type: AssetType;
|
||||
title: string;
|
||||
description?: string;
|
||||
src?: string; // Image URL or Vimeo ID
|
||||
videoTitle?: string;
|
||||
content?: string; // For text based cards
|
||||
script?: string; // The verbal script for sales reps
|
||||
statValue?: string; // For stat cards
|
||||
statLabel?: string;
|
||||
tags: string[];
|
||||
gridSpan?: 'col-span-1' | 'col-span-2'; // For layout control
|
||||
rowSpan?: 'row-span-1' | 'row-span-2';
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
role: 'user' | 'model';
|
||||
text: string;
|
||||
}
|
||||
Reference in New Issue
Block a user