diff --git a/db_test.php b/db_test.php new file mode 100644 index 0000000..e708bbb --- /dev/null +++ b/db_test.php @@ -0,0 +1,27 @@ + 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(); +} +?> diff --git a/ref/.gitignore b/ref/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/ref/.gitignore @@ -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? diff --git a/ref/App.tsx b/ref/App.tsx new file mode 100644 index 0000000..d9af14d --- /dev/null +++ b/ref/App.tsx @@ -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(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 ( +
+ + {/* Toast Notification */} +
+ +
+

Download Started

+

CodeBridgeX_Proposal_v2.4.pdf

+
+
+ + {/* Navbar */} + + + {/* Hero Section */} +
+
+
+
+ + CEO Management Solution +
+

+ 직원의 관리 도구가 아닙니다.
+ + 대표님의 경영 무기입니다. + +

+

+ "SAM"은 단순한 ERP가 아닙니다.
+ 가지급금 이자 계산부터 채권 추심, 실시간 경영 알림까지.
+ 오직 CEO를 위한 시크릿 대시보드를 제안하십시오. +

+
+ + +
+
+
+ + {/* Background Decorations */} +
+
+
+
+
+
+ + {/* Main Grid Content */} +
+
+

Sales Materials

+
+ {['All', 'CEO Logic', 'Legal/Tax', 'Demo'].map((filter) => ( + + ))} +
+
+ + {/* Masonry-like Grid */} +
+ {filteredAssets.map((asset) => ( + setSelectedAsset(a)} + /> + ))} + {filteredAssets.length === 0 && ( +
+ 해당 카테고리에 자료가 없습니다. +
+ )} +
+
+ + {/* Footer */} + + + {/* Detail Modal */} + {selectedAsset && ( + setSelectedAsset(null)} + /> + )} + + {/* AI Assistant */} + +
+ ); +}; + +export default App; diff --git a/ref/README.md b/ref/README.md new file mode 100644 index 0000000..39748c5 --- /dev/null +++ b/ref/README.md @@ -0,0 +1,20 @@ +
+GHBanner +
+ +# 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` diff --git a/ref/components/AssetDetailModal.tsx b/ref/components/AssetDetailModal.tsx new file mode 100644 index 0000000..9ac8d3b --- /dev/null +++ b/ref/components/AssetDetailModal.tsx @@ -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 = ({ 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 ( +
+
+ +
+ + {/* Header (Absolute for Video/Image types to float over, Relative for others) */} + + +
+ {/* Media Section */} +
+ {asset.type === AssetType.VIDEO && ( + + )} + {asset.type === AssetType.IMAGE && ( + {asset.title} + )} + {asset.type === AssetType.STAT && ( +
+
{asset.statValue}
+
{asset.statLabel}
+
+ )} + {asset.type === AssetType.TEXT && ( +
+ +

{asset.title}

+
+ {asset.tags.map(tag => ( + {tag} + ))} +
+
+ )} +
+ + {/* Content Section */} +
+
+ + {/* Left: Info */} +
+

+ {asset.title} + {asset.type !== AssetType.TEXT && ( +
+ {asset.tags.map(tag => ( + + {tag} + + ))} +
+ )} +

+

+ {asset.content || asset.description} +

+ + {/* Additional Metadata if needed */} +
+
+ Asset Type + {asset.type} +
+
+ Last Updated + 2024.05.20 +
+
+
+ + {/* Right: Sales Script (The "Key" feature) */} +
+
+

+ + Sales Pitch Script +

+ +
+
+ +
+ {asset.script ? asset.script : "No specific script available for this asset. Use the description as a guide."} +
+ +
+
+

+ Tip: 고객의 눈을 맞추고 천천히 강조하며 읽으세요. +

+
+
+ +
+
+
+ + {/* Footer Actions */} +
+ + +
+ +
+
+ ); +}; + +export default AssetDetailModal; diff --git a/ref/components/Assistant.tsx b/ref/components/Assistant.tsx new file mode 100644 index 0000000..1ea91bb --- /dev/null +++ b/ref/components/Assistant.tsx @@ -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([ + { role: 'model', text: '안녕하십니까! CodeBridgeX 영업 지원 AI입니다. CEO 설득을 위한 핵심 스크립트나 자료가 필요하신가요?' } + ]); + const [input, setInput] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const messagesEndRef = useRef(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 */} + + + {/* Chat Window */} +
+ {/* Header */} +
+
+ +
+
+

SAM Sales Assistant

+

Powered by Gemini 2.5 Flash

+
+
+ + {/* Messages */} +
+ {messages.map((msg, idx) => ( +
+
+ {msg.text} +
+
+ ))} + {isLoading && ( +
+
+ +
+
+ )} +
+
+ + {/* Input Area */} +
+
+ 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} + /> + +
+
+
+ + ); +}; + +export default Assistant; \ No newline at end of file diff --git a/ref/components/SalesCard.tsx b/ref/components/SalesCard.tsx new file mode 100644 index 0000000..bf1c5c8 --- /dev/null +++ b/ref/components/SalesCard.tsx @@ -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 = ({ 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 ( +
+
+ {/* Pointer events none on iframe to allow card click, or use a cover div */} + +
+
+
+ + Video + +
+
+

{asset.title}

+

{asset.description}

+
+
+ 상세 보기 및 스크립트 +
+
+
+ ); + } + + // Render Image Card + if (asset.type === AssetType.IMAGE) { + return ( +
+
+ {asset.title} +
+
+
+
+ + Image + +
+

{asset.title}

+

{asset.description}

+
+
+ ); + } + + // Render Stat Card + if (asset.type === AssetType.STAT) { + return ( +
+
+ + {asset.tags[0]} +
+
+
{asset.statValue}
+
{asset.statLabel}
+
+
+ * 내부 데이터 기준 + +
+
+ ); + } + + // Render Text Card (Default) + return ( +
+
+
+ {asset.tags.map(tag => ( + + {tag} + + ))} +
+

{asset.title}

+

{asset.content}

+
+
+ 전체 스크립트 보기 + +
+
+ ); +}; + +export default SalesCard; diff --git a/ref/constants.ts b/ref/constants.ts new file mode 100644 index 0000000..9465a63 --- /dev/null +++ b/ref/constants.ts @@ -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' + }, +]; diff --git a/ref/index.html b/ref/index.html new file mode 100644 index 0000000..90279d2 --- /dev/null +++ b/ref/index.html @@ -0,0 +1,63 @@ + + + + + + + SAM - Sales Asset Manager + + + + + + + + +
+ + + \ No newline at end of file diff --git a/ref/index.tsx b/ref/index.tsx new file mode 100644 index 0000000..6ca5361 --- /dev/null +++ b/ref/index.tsx @@ -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( + + + +); \ No newline at end of file diff --git a/ref/metadata.json b/ref/metadata.json new file mode 100644 index 0000000..48796e1 --- /dev/null +++ b/ref/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "SAM - CodeBridgeX Sales", + "description": "Sales enablement platform for CodeBridgeX SAM - The ERP for CEOs.", + "requestFramePermissions": [] +} \ No newline at end of file diff --git a/ref/package.json b/ref/package.json new file mode 100644 index 0000000..364f8d5 --- /dev/null +++ b/ref/package.json @@ -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" + } +} diff --git a/ref/tsconfig.json b/ref/tsconfig.json new file mode 100644 index 0000000..2c6eed5 --- /dev/null +++ b/ref/tsconfig.json @@ -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 + } +} \ No newline at end of file diff --git a/ref/types.ts b/ref/types.ts new file mode 100644 index 0000000..390aa1a --- /dev/null +++ b/ref/types.ts @@ -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; +}