Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- .hfignore +9 -0
- App.tsx +21 -7
- chat_page_check.png +0 -0
- components/ChatPage.tsx +106 -11
- components/ProductGuide.tsx +109 -0
- components/SimulationPage.tsx +200 -12
- package-lock.json +17 -0
- package.json +1 -0
- server.cjs +53 -1
- test-results/.last-run.json +4 -0
- verify_features.spec.ts +54 -0
.hfignore
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
.git
|
| 3 |
+
.github
|
| 4 |
+
*.log
|
| 5 |
+
verify_features.spec.ts
|
| 6 |
+
chat_page_check.png
|
| 7 |
+
.env*
|
| 8 |
+
data
|
| 9 |
+
.DS_Store
|
App.tsx
CHANGED
|
@@ -12,11 +12,11 @@ import Documentation from './components/Documentation';
|
|
| 12 |
import FAQ from './components/FAQ';
|
| 13 |
import Footer from './components/Footer';
|
| 14 |
import SimulationPage from './components/SimulationPage';
|
| 15 |
-
import ConversationPage from './components/ConversationPage';
|
| 16 |
import ChatPage from './components/ChatPage';
|
|
|
|
| 17 |
|
| 18 |
function App() {
|
| 19 |
-
const [currentView, setCurrentView] = useState<'landing' | 'simulation' | '
|
| 20 |
const [user, setUser] = useState<any>(null);
|
| 21 |
const [simulationResult, setSimulationResult] = useState<any>(null);
|
| 22 |
|
|
@@ -59,24 +59,38 @@ function App() {
|
|
| 59 |
setCurrentView('landing');
|
| 60 |
};
|
| 61 |
|
| 62 |
-
const openConversation = () => {
|
| 63 |
-
setCurrentView('conversation');
|
| 64 |
-
};
|
| 65 |
-
|
| 66 |
const openChat = () => {
|
| 67 |
setCurrentView('chat');
|
| 68 |
};
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
const goBackToSimulation = () => {
|
| 71 |
setCurrentView('simulation');
|
| 72 |
};
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
if (currentView === 'simulation') {
|
| 75 |
return (
|
| 76 |
<SimulationPage
|
| 77 |
onBack={goBackToLanding}
|
| 78 |
-
onOpenConversation={openConversation}
|
| 79 |
onOpenChat={openChat}
|
|
|
|
| 80 |
user={user}
|
| 81 |
onLogin={loginWithHF}
|
| 82 |
onLogout={handleLogout}
|
|
|
|
| 12 |
import FAQ from './components/FAQ';
|
| 13 |
import Footer from './components/Footer';
|
| 14 |
import SimulationPage from './components/SimulationPage';
|
|
|
|
| 15 |
import ChatPage from './components/ChatPage';
|
| 16 |
+
import ProductGuide from './components/ProductGuide';
|
| 17 |
|
| 18 |
function App() {
|
| 19 |
+
const [currentView, setCurrentView] = useState<'landing' | 'simulation' | 'chat' | 'guide'>('simulation');
|
| 20 |
const [user, setUser] = useState<any>(null);
|
| 21 |
const [simulationResult, setSimulationResult] = useState<any>(null);
|
| 22 |
|
|
|
|
| 59 |
setCurrentView('landing');
|
| 60 |
};
|
| 61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
const openChat = () => {
|
| 63 |
setCurrentView('chat');
|
| 64 |
};
|
| 65 |
|
| 66 |
+
const openGuide = () => {
|
| 67 |
+
setCurrentView('guide');
|
| 68 |
+
};
|
| 69 |
+
|
| 70 |
const goBackToSimulation = () => {
|
| 71 |
setCurrentView('simulation');
|
| 72 |
};
|
| 73 |
|
| 74 |
+
if (currentView === 'guide') {
|
| 75 |
+
return (
|
| 76 |
+
<div className="bg-black min-h-screen relative">
|
| 77 |
+
<button
|
| 78 |
+
onClick={goBackToSimulation}
|
| 79 |
+
className="absolute top-8 left-8 p-3 bg-gray-900 border border-gray-800 rounded-full text-white hover:bg-gray-800 transition-colors z-50"
|
| 80 |
+
>
|
| 81 |
+
<X size={24} />
|
| 82 |
+
</button>
|
| 83 |
+
<ProductGuide />
|
| 84 |
+
</div>
|
| 85 |
+
);
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
if (currentView === 'simulation') {
|
| 89 |
return (
|
| 90 |
<SimulationPage
|
| 91 |
onBack={goBackToLanding}
|
|
|
|
| 92 |
onOpenChat={openChat}
|
| 93 |
+
onOpenGuide={openGuide}
|
| 94 |
user={user}
|
| 95 |
onLogin={loginWithHF}
|
| 96 |
onLogout={handleLogout}
|
chat_page_check.png
ADDED
|
components/ChatPage.tsx
CHANGED
|
@@ -78,7 +78,7 @@ const ChatInput: React.FC<{ onSimulate: (msg: string) => void; onHelpMeCraft: (m
|
|
| 78 |
};
|
| 79 |
|
| 80 |
return (
|
| 81 |
-
<div className="border-t border-gray-800 pt-6
|
| 82 |
<div className="max-w-5xl mx-auto space-y-4">
|
| 83 |
{uploadedFiles.length > 0 && (
|
| 84 |
<div className="flex flex-wrap gap-2 mb-2">
|
|
@@ -153,6 +153,8 @@ const ChatPage: React.FC<ChatPageProps> = ({ onBack, simulationResult, setSimula
|
|
| 153 |
const [isSimulating, setIsSimulating] = useState(false);
|
| 154 |
const [simulationId, setSimulationId] = useState<string>('User Group 1');
|
| 155 |
const [selectedVariation, setSelectedVariation] = useState<string>('');
|
|
|
|
|
|
|
| 156 |
|
| 157 |
useEffect(() => {
|
| 158 |
const fetchSimulations = async () => {
|
|
@@ -183,11 +185,27 @@ const ChatPage: React.FC<ChatPageProps> = ({ onBack, simulationResult, setSimula
|
|
| 183 |
try {
|
| 184 |
const result = await GradioService.startSimulationAsync(simulationId, msg);
|
| 185 |
setIsSimulating(false);
|
| 186 |
-
|
| 187 |
status: "Initiated",
|
| 188 |
message: "Simulation started successfully. Please wait for the results.",
|
| 189 |
-
data: result
|
| 190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
} catch (error) {
|
| 192 |
setIsSimulating(false);
|
| 193 |
setSimulationResult({
|
|
@@ -202,11 +220,25 @@ const ChatPage: React.FC<ChatPageProps> = ({ onBack, simulationResult, setSimula
|
|
| 202 |
try {
|
| 203 |
const status = await GradioService.getSimulationStatus(simulationId);
|
| 204 |
setIsSimulating(false);
|
| 205 |
-
|
| 206 |
status: "Updated",
|
| 207 |
message: "Latest status gathered from API.",
|
| 208 |
-
data: status
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
} catch (error) {
|
| 211 |
setIsSimulating(false);
|
| 212 |
setSimulationResult({
|
|
@@ -273,7 +305,7 @@ const ChatPage: React.FC<ChatPageProps> = ({ onBack, simulationResult, setSimula
|
|
| 273 |
};
|
| 274 |
|
| 275 |
return (
|
| 276 |
-
<div className="fixed inset-0 z-50 bg-[#050505] text-white flex flex-col animate-in fade-in duration-300">
|
| 277 |
|
| 278 |
{/* Header / Nav */}
|
| 279 |
<div className="flex items-center justify-between px-6 py-4 md:px-8 md:py-6 border-b border-gray-800/50 bg-[#050505] z-10 relative">
|
|
@@ -328,8 +360,8 @@ const ChatPage: React.FC<ChatPageProps> = ({ onBack, simulationResult, setSimula
|
|
| 328 |
)}
|
| 329 |
|
| 330 |
{/* Scrollable Content Area */}
|
| 331 |
-
<div className="flex-1 overflow-y-auto custom-scrollbar p-6 md:p-8
|
| 332 |
-
<div className="max-w-5xl mx-auto">
|
| 333 |
<h1 className="text-2xl md:text-3xl font-semibold text-center mb-12 mt-4 md:mt-8">What would you like to simulate?</h1>
|
| 334 |
|
| 335 |
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-12">
|
|
@@ -352,7 +384,10 @@ const ChatPage: React.FC<ChatPageProps> = ({ onBack, simulationResult, setSimula
|
|
| 352 |
</div>
|
| 353 |
|
| 354 |
<div className="flex justify-center mt-16 mb-8">
|
| 355 |
-
<button
|
|
|
|
|
|
|
|
|
|
| 356 |
<Plus size={16} />
|
| 357 |
Request a new context
|
| 358 |
</button>
|
|
@@ -360,6 +395,66 @@ const ChatPage: React.FC<ChatPageProps> = ({ onBack, simulationResult, setSimula
|
|
| 360 |
</div>
|
| 361 |
</div>
|
| 362 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
{/* Input Footer */}
|
| 364 |
<ChatInput onSimulate={handleSimulate} onHelpMeCraft={handleHelpMeCraft} isSimulating={isSimulating} />
|
| 365 |
</div>
|
|
|
|
| 78 |
};
|
| 79 |
|
| 80 |
return (
|
| 81 |
+
<div className="border-t border-gray-800 pt-6 bg-[#0a0a0a] px-6 pb-8 md:pb-10 z-20 shadow-[0_-20px_50px_rgba(0,0,0,0.8)]">
|
| 82 |
<div className="max-w-5xl mx-auto space-y-4">
|
| 83 |
{uploadedFiles.length > 0 && (
|
| 84 |
<div className="flex flex-wrap gap-2 mb-2">
|
|
|
|
| 153 |
const [isSimulating, setIsSimulating] = useState(false);
|
| 154 |
const [simulationId, setSimulationId] = useState<string>('User Group 1');
|
| 155 |
const [selectedVariation, setSelectedVariation] = useState<string>('');
|
| 156 |
+
const [showContextModal, setShowContextModal] = useState(false);
|
| 157 |
+
const [contextInput, setContextInput] = useState('');
|
| 158 |
|
| 159 |
useEffect(() => {
|
| 160 |
const fetchSimulations = async () => {
|
|
|
|
| 185 |
try {
|
| 186 |
const result = await GradioService.startSimulationAsync(simulationId, msg);
|
| 187 |
setIsSimulating(false);
|
| 188 |
+
const resData = {
|
| 189 |
status: "Initiated",
|
| 190 |
message: "Simulation started successfully. Please wait for the results.",
|
| 191 |
+
data: result,
|
| 192 |
+
content: msg,
|
| 193 |
+
variation: selectedVariation,
|
| 194 |
+
simulationId
|
| 195 |
+
};
|
| 196 |
+
setSimulationResult(resData);
|
| 197 |
+
|
| 198 |
+
// Persist simulation initiation
|
| 199 |
+
fetch('/api/save-data', {
|
| 200 |
+
method: 'POST',
|
| 201 |
+
headers: { 'Content-Type': 'application/json' },
|
| 202 |
+
body: JSON.stringify({
|
| 203 |
+
type: 'simulation-start',
|
| 204 |
+
data: resData,
|
| 205 |
+
user: 'chat-user'
|
| 206 |
+
})
|
| 207 |
+
}).catch(console.error);
|
| 208 |
+
|
| 209 |
} catch (error) {
|
| 210 |
setIsSimulating(false);
|
| 211 |
setSimulationResult({
|
|
|
|
| 220 |
try {
|
| 221 |
const status = await GradioService.getSimulationStatus(simulationId);
|
| 222 |
setIsSimulating(false);
|
| 223 |
+
const resData = {
|
| 224 |
status: "Updated",
|
| 225 |
message: "Latest status gathered from API.",
|
| 226 |
+
data: status,
|
| 227 |
+
simulationId
|
| 228 |
+
};
|
| 229 |
+
setSimulationResult(resData);
|
| 230 |
+
|
| 231 |
+
// Persist simulation result
|
| 232 |
+
fetch('/api/save-data', {
|
| 233 |
+
method: 'POST',
|
| 234 |
+
headers: { 'Content-Type': 'application/json' },
|
| 235 |
+
body: JSON.stringify({
|
| 236 |
+
type: 'simulation-result',
|
| 237 |
+
data: resData,
|
| 238 |
+
user: 'chat-user'
|
| 239 |
+
})
|
| 240 |
+
}).catch(console.error);
|
| 241 |
+
|
| 242 |
} catch (error) {
|
| 243 |
setIsSimulating(false);
|
| 244 |
setSimulationResult({
|
|
|
|
| 305 |
};
|
| 306 |
|
| 307 |
return (
|
| 308 |
+
<div className="fixed inset-0 z-50 bg-[#050505] text-white flex flex-col animate-in fade-in duration-300 overflow-hidden">
|
| 309 |
|
| 310 |
{/* Header / Nav */}
|
| 311 |
<div className="flex items-center justify-between px-6 py-4 md:px-8 md:py-6 border-b border-gray-800/50 bg-[#050505] z-10 relative">
|
|
|
|
| 360 |
)}
|
| 361 |
|
| 362 |
{/* Scrollable Content Area */}
|
| 363 |
+
<div className="flex-1 overflow-y-auto custom-scrollbar p-6 md:p-8">
|
| 364 |
+
<div className="max-w-5xl mx-auto pb-20">
|
| 365 |
<h1 className="text-2xl md:text-3xl font-semibold text-center mb-12 mt-4 md:mt-8">What would you like to simulate?</h1>
|
| 366 |
|
| 367 |
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-12">
|
|
|
|
| 384 |
</div>
|
| 385 |
|
| 386 |
<div className="flex justify-center mt-16 mb-8">
|
| 387 |
+
<button
|
| 388 |
+
onClick={() => setShowContextModal(true)}
|
| 389 |
+
className="flex items-center gap-2 text-gray-500 hover:text-gray-300 transition-colors text-sm px-4 py-2 hover:bg-gray-900 rounded-lg"
|
| 390 |
+
>
|
| 391 |
<Plus size={16} />
|
| 392 |
Request a new context
|
| 393 |
</button>
|
|
|
|
| 395 |
</div>
|
| 396 |
</div>
|
| 397 |
|
| 398 |
+
{/* Context Modal */}
|
| 399 |
+
{showContextModal && (
|
| 400 |
+
<div className="absolute inset-0 z-[60] flex items-center justify-center p-6 bg-black/60 backdrop-blur-sm">
|
| 401 |
+
<div className="bg-[#111] border border-gray-800 rounded-2xl w-full max-w-md overflow-hidden shadow-2xl">
|
| 402 |
+
<div className="p-6 border-b border-gray-800 flex items-center justify-between">
|
| 403 |
+
<h3 className="font-semibold text-lg">Request New Context</h3>
|
| 404 |
+
<button onClick={() => setShowContextModal(false)} className="text-gray-500 hover:text-white">
|
| 405 |
+
<X size={20} />
|
| 406 |
+
</button>
|
| 407 |
+
</div>
|
| 408 |
+
<div className="p-6 space-y-4">
|
| 409 |
+
<div className="space-y-1.5">
|
| 410 |
+
<label className="text-xs text-gray-400 font-medium">New Context / Fuse Box</label>
|
| 411 |
+
<textarea
|
| 412 |
+
value={contextInput}
|
| 413 |
+
onChange={(e) => setContextInput(e.target.value)}
|
| 414 |
+
className="w-full bg-black border border-gray-800 rounded-lg p-3 text-sm focus:border-teal-500 outline-none h-40 resize-none"
|
| 415 |
+
placeholder="Specify the testing environment or scenario..."
|
| 416 |
+
/>
|
| 417 |
+
</div>
|
| 418 |
+
</div>
|
| 419 |
+
<div className="p-6 border-t border-gray-800 flex gap-3">
|
| 420 |
+
<button
|
| 421 |
+
onClick={() => setShowContextModal(false)}
|
| 422 |
+
className="flex-1 py-2.5 rounded-xl border border-gray-800 text-sm font-medium hover:bg-gray-900 transition-colors"
|
| 423 |
+
>
|
| 424 |
+
Cancel
|
| 425 |
+
</button>
|
| 426 |
+
<button
|
| 427 |
+
onClick={async () => {
|
| 428 |
+
try {
|
| 429 |
+
const response = await fetch('/api/save-data', {
|
| 430 |
+
method: 'POST',
|
| 431 |
+
headers: { 'Content-Type': 'application/json' },
|
| 432 |
+
body: JSON.stringify({
|
| 433 |
+
type: 'context-request',
|
| 434 |
+
data: { context: contextInput },
|
| 435 |
+
user: 'chat-user'
|
| 436 |
+
})
|
| 437 |
+
});
|
| 438 |
+
if (response.ok) {
|
| 439 |
+
alert('Context request saved!');
|
| 440 |
+
setShowContextModal(false);
|
| 441 |
+
setContextInput('');
|
| 442 |
+
}
|
| 443 |
+
} catch (e) {
|
| 444 |
+
console.error(e);
|
| 445 |
+
alert('Successfully submitted (Local simulation)');
|
| 446 |
+
setShowContextModal(false);
|
| 447 |
+
}
|
| 448 |
+
}}
|
| 449 |
+
className="flex-1 py-2.5 rounded-xl bg-teal-600 text-white text-sm font-bold hover:bg-teal-500 transition-colors shadow-lg shadow-teal-900/20"
|
| 450 |
+
>
|
| 451 |
+
Confirm
|
| 452 |
+
</button>
|
| 453 |
+
</div>
|
| 454 |
+
</div>
|
| 455 |
+
</div>
|
| 456 |
+
)}
|
| 457 |
+
|
| 458 |
{/* Input Footer */}
|
| 459 |
<ChatInput onSimulate={handleSimulate} onHelpMeCraft={handleHelpMeCraft} isSimulating={isSimulating} />
|
| 460 |
</div>
|
components/ProductGuide.tsx
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
import { Book, Code, Terminal, Zap, ChevronRight, CheckCircle, Info } from 'lucide-react';
|
| 3 |
+
|
| 4 |
+
const ProductGuide: React.FC = () => {
|
| 5 |
+
return (
|
| 6 |
+
<div className="bg-black min-h-screen text-white p-8 md:p-16 max-w-5xl mx-auto font-sans">
|
| 7 |
+
<h1 className="text-4xl md:text-6xl font-bold mb-8 bg-gradient-to-r from-teal-400 to-blue-500 bg-clip-text text-transparent">
|
| 8 |
+
Product Guide & Documentation
|
| 9 |
+
</h1>
|
| 10 |
+
|
| 11 |
+
<p className="text-xl text-gray-400 mb-12 leading-relaxed">
|
| 12 |
+
Welcome to Branding Content Testing. This guide will help you understand how to use our agentic simulation platform to validate your brand narratives and marketing content before going live.
|
| 13 |
+
</p>
|
| 14 |
+
|
| 15 |
+
<div className="space-y-16">
|
| 16 |
+
{/* Section 1 */}
|
| 17 |
+
<section>
|
| 18 |
+
<div className="flex items-center gap-4 mb-6">
|
| 19 |
+
<div className="w-10 h-10 rounded-xl bg-teal-500/20 border border-teal-500/50 flex items-center justify-center text-teal-400">
|
| 20 |
+
<Zap size={20} />
|
| 21 |
+
</div>
|
| 22 |
+
<h2 className="text-2xl font-bold">Getting Started</h2>
|
| 23 |
+
</div>
|
| 24 |
+
<div className="prose prose-invert max-w-none text-gray-300 space-y-4">
|
| 25 |
+
<p>
|
| 26 |
+
To start using the platform, you must first authenticate with your Hugging Face account. This allows you to save your simulations, custom focus groups, and results.
|
| 27 |
+
</p>
|
| 28 |
+
<div className="bg-gray-900/50 border border-gray-800 rounded-xl p-6">
|
| 29 |
+
<h3 className="text-white font-semibold mb-2 flex items-center gap-2">
|
| 30 |
+
<CheckCircle size={16} className="text-green-500" />
|
| 31 |
+
Step 1: Sign in with Hugging Face
|
| 32 |
+
</h3>
|
| 33 |
+
<p className="text-sm">Click the "Sign in with Hugging Face" button in the sidebar or navbar. You will be redirected to Hugging Face to authorize the application.</p>
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
</section>
|
| 37 |
+
|
| 38 |
+
{/* Section 2 */}
|
| 39 |
+
<section>
|
| 40 |
+
<div className="flex items-center gap-4 mb-6">
|
| 41 |
+
<div className="w-10 h-10 rounded-xl bg-blue-500/20 border border-blue-500/50 flex items-center justify-center text-blue-400">
|
| 42 |
+
<Book size={20} />
|
| 43 |
+
</div>
|
| 44 |
+
<h2 className="text-2xl font-bold">Core Concepts</h2>
|
| 45 |
+
</div>
|
| 46 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
| 47 |
+
<div className="bg-[#0a0a0a] border border-gray-800 p-6 rounded-2xl">
|
| 48 |
+
<h3 className="text-lg font-bold mb-3">Focus Groups</h3>
|
| 49 |
+
<p className="text-sm text-gray-400">AI-generated collectives of personas that mirror specific real-world audiences. You can create your own by describing your target customer and company profile.</p>
|
| 50 |
+
</div>
|
| 51 |
+
<div className="bg-[#0a0a0a] border border-gray-800 p-6 rounded-2xl">
|
| 52 |
+
<h3 className="text-lg font-bold mb-3">Simulations</h3>
|
| 53 |
+
<p className="text-sm text-gray-400">The process of running your content through a Focus Group to gather sentiment, engagement predictions, and feedback.</p>
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
</section>
|
| 57 |
+
|
| 58 |
+
{/* Section 3 */}
|
| 59 |
+
<section>
|
| 60 |
+
<div className="flex items-center gap-4 mb-6">
|
| 61 |
+
<div className="w-10 h-10 rounded-xl bg-purple-500/20 border border-purple-500/50 flex items-center justify-center text-purple-400">
|
| 62 |
+
<Terminal size={20} />
|
| 63 |
+
</div>
|
| 64 |
+
<h2 className="text-2xl font-bold">How to Create a Focus Group</h2>
|
| 65 |
+
</div>
|
| 66 |
+
<ul className="space-y-4 text-gray-400">
|
| 67 |
+
<li className="flex items-start gap-3">
|
| 68 |
+
<ChevronRight size={18} className="text-teal-500 mt-1" />
|
| 69 |
+
<span><strong>Customer Profile:</strong> Describe who your ideal customer is (e.g., "Tech founders in Europe interested in sustainability").</span>
|
| 70 |
+
</li>
|
| 71 |
+
<li className="flex items-start gap-3">
|
| 72 |
+
<ChevronRight size={18} className="text-teal-500 mt-1" />
|
| 73 |
+
<span><strong>Company Info:</strong> Provide context about your brand and what you offer.</span>
|
| 74 |
+
</li>
|
| 75 |
+
<li className="flex items-start gap-3">
|
| 76 |
+
<ChevronRight size={18} className="text-teal-500 mt-1" />
|
| 77 |
+
<span><strong>Persona Scale:</strong> Adjust the scale (1-100) to determine how many unique personas should be generated for the group.</span>
|
| 78 |
+
</li>
|
| 79 |
+
</ul>
|
| 80 |
+
</section>
|
| 81 |
+
|
| 82 |
+
{/* FAQ Section */}
|
| 83 |
+
<section className="bg-teal-950/10 border border-teal-900/30 rounded-3xl p-8 md:p-12">
|
| 84 |
+
<h2 className="text-3xl font-bold mb-8">Frequently Asked Questions</h2>
|
| 85 |
+
<div className="space-y-8">
|
| 86 |
+
<div>
|
| 87 |
+
<h4 className="text-teal-400 font-semibold mb-2">How long does a simulation take?</h4>
|
| 88 |
+
<p className="text-gray-400">Simulations run asynchronously and can take up to 30 minutes to fully process. Use the "Gather Results" button to fetch updates.</p>
|
| 89 |
+
</div>
|
| 90 |
+
<div>
|
| 91 |
+
<h4 className="text-teal-400 font-semibold mb-2">Can I upload images?</h4>
|
| 92 |
+
<p className="text-gray-400">Yes! You can upload local images or provide web image links to test visual brand assets and website layouts.</p>
|
| 93 |
+
</div>
|
| 94 |
+
</div>
|
| 95 |
+
</section>
|
| 96 |
+
</div>
|
| 97 |
+
|
| 98 |
+
<div className="mt-20 pt-12 border-t border-gray-900 text-center">
|
| 99 |
+
<p className="text-gray-500 text-sm mb-6">Need more help? Join our community or contact support.</p>
|
| 100 |
+
<div className="flex justify-center gap-4">
|
| 101 |
+
<div className="px-6 py-2 bg-white text-black rounded-full font-bold text-sm cursor-pointer hover:bg-gray-200 transition-colors">Contact Support</div>
|
| 102 |
+
<div className="px-6 py-2 border border-gray-700 rounded-full font-bold text-sm cursor-pointer hover:bg-gray-800 transition-colors">API Docs</div>
|
| 103 |
+
</div>
|
| 104 |
+
</div>
|
| 105 |
+
</div>
|
| 106 |
+
);
|
| 107 |
+
};
|
| 108 |
+
|
| 109 |
+
export default ProductGuide;
|
components/SimulationPage.tsx
CHANGED
|
@@ -5,8 +5,8 @@ import { GradioService } from '../services/gradioService';
|
|
| 5 |
|
| 6 |
interface SimulationPageProps {
|
| 7 |
onBack: () => void;
|
| 8 |
-
onOpenConversation: () => void;
|
| 9 |
onOpenChat: () => void;
|
|
|
|
| 10 |
user?: any;
|
| 11 |
onLogin?: () => void;
|
| 12 |
onLogout?: () => void;
|
|
@@ -45,7 +45,7 @@ const VIEW_FILTERS: Record<string, Array<{ label: string; color: string }>> = {
|
|
| 45 |
};
|
| 46 |
|
| 47 |
const SimulationPage: React.FC<SimulationPageProps> = ({
|
| 48 |
-
onBack,
|
| 49 |
}) => {
|
| 50 |
const [society, setSociety] = useState('');
|
| 51 |
const [societies, setSocieties] = useState<string[]>([]);
|
|
@@ -55,6 +55,16 @@ const SimulationPage: React.FC<SimulationPageProps> = ({
|
|
| 55 |
const [isRightPanelOpen, setIsRightPanelOpen] = useState(window.innerWidth > 1200);
|
| 56 |
const [isLeftPanelOpen, setIsLeftPanelOpen] = useState(window.innerWidth > 768);
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
// Handle window resize for mobile responsiveness
|
| 59 |
useEffect(() => {
|
| 60 |
const handleResize = () => {
|
|
@@ -68,23 +78,45 @@ const SimulationPage: React.FC<SimulationPageProps> = ({
|
|
| 68 |
// Fetch real focus groups
|
| 69 |
const fetchSocieties = async () => {
|
| 70 |
try {
|
|
|
|
|
|
|
|
|
|
| 71 |
const result = await GradioService.listSimulations();
|
| 72 |
-
// Handle both direct array and Gradio data wrap
|
| 73 |
const list = Array.isArray(result) ? result : (result?.data?.[0] || []);
|
| 74 |
|
| 75 |
if (Array.isArray(list)) {
|
| 76 |
-
const
|
| 77 |
.map((s: any) => {
|
| 78 |
if (typeof s === 'string') return s;
|
| 79 |
if (typeof s === 'object' && s !== null) return s.id || s.name || '';
|
| 80 |
return '';
|
| 81 |
})
|
|
|
|
| 82 |
.filter(name => name.length > 0 && !name.toLowerCase().includes('default') && !name.toLowerCase().includes('template') && !name.toLowerCase().includes('current'));
|
| 83 |
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
}
|
| 89 |
} catch (e) {
|
| 90 |
console.error("Failed to fetch focus groups", e);
|
|
@@ -167,7 +199,7 @@ const SimulationPage: React.FC<SimulationPageProps> = ({
|
|
| 167 |
|
| 168 |
{/* Actions */}
|
| 169 |
<button
|
| 170 |
-
onClick={
|
| 171 |
className="w-full flex items-center justify-between text-left text-sm text-gray-300 hover:text-white group py-2 border-b border-gray-800/50 mb-1"
|
| 172 |
>
|
| 173 |
<span>Assemble new group</span>
|
|
@@ -175,13 +207,21 @@ const SimulationPage: React.FC<SimulationPageProps> = ({
|
|
| 175 |
</button>
|
| 176 |
|
| 177 |
<button
|
| 178 |
-
onClick={
|
| 179 |
-
className="w-full flex items-center justify-between text-left text-sm text-gray-300 hover:text-white group py-2"
|
| 180 |
>
|
| 181 |
<span>Create a new test</span>
|
| 182 |
<Plus size={18} className="text-gray-500 group-hover:text-white" />
|
| 183 |
</button>
|
| 184 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
{/* Global Chat Button (Sidebar) */}
|
| 186 |
<button
|
| 187 |
onClick={onOpenChat}
|
|
@@ -238,8 +278,8 @@ const SimulationPage: React.FC<SimulationPageProps> = ({
|
|
| 238 |
</div>
|
| 239 |
)}
|
| 240 |
|
| 241 |
-
<MenuItem icon={<MessageSquare size={16}/>} label="Leave Feedback" />
|
| 242 |
-
<MenuItem icon={<BookOpen size={16}/>} label="Product Guide" />
|
| 243 |
{user && <MenuItem icon={<LogOut size={16}/>} label="Log Out" onClick={onLogout} />}
|
| 244 |
|
| 245 |
<div className="pt-4 text-[10px] text-gray-600">Version 2.1</div>
|
|
@@ -281,6 +321,154 @@ const SimulationPage: React.FC<SimulationPageProps> = ({
|
|
| 281 |
<SimulationGraph isBuilding={isBuilding} societyType={society} onStartChat={onOpenChat} />
|
| 282 |
</div>
|
| 283 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
{/* Floating Chat Button (Bottom) */}
|
| 285 |
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 z-30">
|
| 286 |
<button
|
|
|
|
| 5 |
|
| 6 |
interface SimulationPageProps {
|
| 7 |
onBack: () => void;
|
|
|
|
| 8 |
onOpenChat: () => void;
|
| 9 |
+
onOpenGuide: () => void;
|
| 10 |
user?: any;
|
| 11 |
onLogin?: () => void;
|
| 12 |
onLogout?: () => void;
|
|
|
|
| 45 |
};
|
| 46 |
|
| 47 |
const SimulationPage: React.FC<SimulationPageProps> = ({
|
| 48 |
+
onBack, onOpenChat, onOpenGuide, user, onLogin, onLogout, simulationResult, setSimulationResult
|
| 49 |
}) => {
|
| 50 |
const [society, setSociety] = useState('');
|
| 51 |
const [societies, setSocieties] = useState<string[]>([]);
|
|
|
|
| 55 |
const [isRightPanelOpen, setIsRightPanelOpen] = useState(window.innerWidth > 1200);
|
| 56 |
const [isLeftPanelOpen, setIsLeftPanelOpen] = useState(window.innerWidth > 768);
|
| 57 |
|
| 58 |
+
const [activeModal, setActiveModal] = useState<'none' | 'assemble' | 'feedback' | 'context' | 'test'>('none');
|
| 59 |
+
const [formData, setFormData] = useState({
|
| 60 |
+
customerProfile: '',
|
| 61 |
+
companyInfo: '',
|
| 62 |
+
personaScale: 50,
|
| 63 |
+
feedback: '',
|
| 64 |
+
context: '',
|
| 65 |
+
testName: ''
|
| 66 |
+
});
|
| 67 |
+
|
| 68 |
// Handle window resize for mobile responsiveness
|
| 69 |
useEffect(() => {
|
| 70 |
const handleResize = () => {
|
|
|
|
| 78 |
// Fetch real focus groups
|
| 79 |
const fetchSocieties = async () => {
|
| 80 |
try {
|
| 81 |
+
let names: string[] = [];
|
| 82 |
+
|
| 83 |
+
// 1. Fetch from Gradio (Templates/Global)
|
| 84 |
const result = await GradioService.listSimulations();
|
|
|
|
| 85 |
const list = Array.isArray(result) ? result : (result?.data?.[0] || []);
|
| 86 |
|
| 87 |
if (Array.isArray(list)) {
|
| 88 |
+
const gradioNames = list
|
| 89 |
.map((s: any) => {
|
| 90 |
if (typeof s === 'string') return s;
|
| 91 |
if (typeof s === 'object' && s !== null) return s.id || s.name || '';
|
| 92 |
return '';
|
| 93 |
})
|
| 94 |
+
// Filter out non-user groups as requested
|
| 95 |
.filter(name => name.length > 0 && !name.toLowerCase().includes('default') && !name.toLowerCase().includes('template') && !name.toLowerCase().includes('current'));
|
| 96 |
|
| 97 |
+
names = [...gradioNames];
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// 2. Fetch User-created groups from local storage
|
| 101 |
+
if (user?.preferred_username) {
|
| 102 |
+
try {
|
| 103 |
+
const localResp = await fetch(`/api/list-data?type=assemble&user=${user.preferred_username}`);
|
| 104 |
+
if (localResp.ok) {
|
| 105 |
+
const localData = await localResp.json();
|
| 106 |
+
const localNames = localData.map((d: any) => d.data.customerProfile.substring(0, 20) + '...');
|
| 107 |
+
names = [...names, ...localNames];
|
| 108 |
}
|
| 109 |
+
} catch (e) {
|
| 110 |
+
console.error("Failed to fetch local groups", e);
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
// Remove duplicates
|
| 115 |
+
const uniqueNames = Array.from(new Set(names));
|
| 116 |
+
setSocieties(uniqueNames);
|
| 117 |
+
|
| 118 |
+
if (uniqueNames.length > 0 && (!society || !uniqueNames.includes(society))) {
|
| 119 |
+
setSociety(uniqueNames[0]);
|
| 120 |
}
|
| 121 |
} catch (e) {
|
| 122 |
console.error("Failed to fetch focus groups", e);
|
|
|
|
| 199 |
|
| 200 |
{/* Actions */}
|
| 201 |
<button
|
| 202 |
+
onClick={() => setActiveModal('assemble')}
|
| 203 |
className="w-full flex items-center justify-between text-left text-sm text-gray-300 hover:text-white group py-2 border-b border-gray-800/50 mb-1"
|
| 204 |
>
|
| 205 |
<span>Assemble new group</span>
|
|
|
|
| 207 |
</button>
|
| 208 |
|
| 209 |
<button
|
| 210 |
+
onClick={() => setActiveModal('test')}
|
| 211 |
+
className="w-full flex items-center justify-between text-left text-sm text-gray-300 hover:text-white group py-2 border-b border-gray-800/50 mb-1"
|
| 212 |
>
|
| 213 |
<span>Create a new test</span>
|
| 214 |
<Plus size={18} className="text-gray-500 group-hover:text-white" />
|
| 215 |
</button>
|
| 216 |
|
| 217 |
+
<button
|
| 218 |
+
onClick={() => setActiveModal('context')}
|
| 219 |
+
className="w-full flex items-center justify-between text-left text-sm text-gray-300 hover:text-white group py-2"
|
| 220 |
+
>
|
| 221 |
+
<span>Request new context</span>
|
| 222 |
+
<Plus size={18} className="text-gray-500 group-hover:text-white" />
|
| 223 |
+
</button>
|
| 224 |
+
|
| 225 |
{/* Global Chat Button (Sidebar) */}
|
| 226 |
<button
|
| 227 |
onClick={onOpenChat}
|
|
|
|
| 278 |
</div>
|
| 279 |
)}
|
| 280 |
|
| 281 |
+
<MenuItem icon={<MessageSquare size={16}/>} label="Leave Feedback" onClick={() => setActiveModal('feedback')} />
|
| 282 |
+
<MenuItem icon={<BookOpen size={16}/>} label="Product Guide" onClick={onOpenGuide} />
|
| 283 |
{user && <MenuItem icon={<LogOut size={16}/>} label="Log Out" onClick={onLogout} />}
|
| 284 |
|
| 285 |
<div className="pt-4 text-[10px] text-gray-600">Version 2.1</div>
|
|
|
|
| 321 |
<SimulationGraph isBuilding={isBuilding} societyType={society} onStartChat={onOpenChat} />
|
| 322 |
</div>
|
| 323 |
|
| 324 |
+
{/* Modals */}
|
| 325 |
+
{activeModal !== 'none' && (
|
| 326 |
+
<div className="absolute inset-0 z-[60] flex items-center justify-center p-6 bg-black/60 backdrop-blur-sm">
|
| 327 |
+
<div className="bg-[#111] border border-gray-800 rounded-2xl w-full max-w-md overflow-hidden shadow-2xl">
|
| 328 |
+
<div className="p-6 border-b border-gray-800 flex items-center justify-between">
|
| 329 |
+
<h3 className="font-semibold text-lg">
|
| 330 |
+
{activeModal === 'assemble' && "Assemble New Group"}
|
| 331 |
+
{activeModal === 'feedback' && "Leave Feedback"}
|
| 332 |
+
{activeModal === 'context' && "Request New Context"}
|
| 333 |
+
{activeModal === 'test' && "Create New Test"}
|
| 334 |
+
</h3>
|
| 335 |
+
<button onClick={() => setActiveModal('none')} className="text-gray-500 hover:text-white">
|
| 336 |
+
<PanelRightClose size={20} />
|
| 337 |
+
</button>
|
| 338 |
+
</div>
|
| 339 |
+
|
| 340 |
+
<div className="p-6 space-y-4">
|
| 341 |
+
{activeModal === 'assemble' && (
|
| 342 |
+
<>
|
| 343 |
+
<div className="space-y-1.5">
|
| 344 |
+
<label className="text-xs text-gray-400 font-medium">Customer Profile</label>
|
| 345 |
+
<textarea
|
| 346 |
+
value={formData.customerProfile}
|
| 347 |
+
onChange={(e) => setFormData({...formData, customerProfile: e.target.value})}
|
| 348 |
+
className="w-full bg-black border border-gray-800 rounded-lg p-3 text-sm focus:border-teal-500 outline-none h-24 resize-none"
|
| 349 |
+
placeholder="Describe your ideal audience..."
|
| 350 |
+
/>
|
| 351 |
+
</div>
|
| 352 |
+
<div className="space-y-1.5">
|
| 353 |
+
<label className="text-xs text-gray-400 font-medium">Company Info</label>
|
| 354 |
+
<textarea
|
| 355 |
+
value={formData.companyInfo}
|
| 356 |
+
onChange={(e) => setFormData({...formData, companyInfo: e.target.value})}
|
| 357 |
+
className="w-full bg-black border border-gray-800 rounded-lg p-3 text-sm focus:border-teal-500 outline-none h-24 resize-none"
|
| 358 |
+
placeholder="Tell us about your brand..."
|
| 359 |
+
/>
|
| 360 |
+
</div>
|
| 361 |
+
<div className="space-y-1.5">
|
| 362 |
+
<div className="flex justify-between">
|
| 363 |
+
<label className="text-xs text-gray-400 font-medium">Persona Scale</label>
|
| 364 |
+
<span className="text-xs text-teal-500 font-bold">{formData.personaScale}</span>
|
| 365 |
+
</div>
|
| 366 |
+
<input
|
| 367 |
+
type="range" min="1" max="100"
|
| 368 |
+
value={formData.personaScale}
|
| 369 |
+
onChange={(e) => setFormData({...formData, personaScale: parseInt(e.target.value)})}
|
| 370 |
+
className="w-full accent-teal-500"
|
| 371 |
+
/>
|
| 372 |
+
<div className="flex justify-between text-[10px] text-gray-600 uppercase font-bold">
|
| 373 |
+
<span>Conservative</span>
|
| 374 |
+
<span>Radical</span>
|
| 375 |
+
</div>
|
| 376 |
+
</div>
|
| 377 |
+
</>
|
| 378 |
+
)}
|
| 379 |
+
|
| 380 |
+
{activeModal === 'feedback' && (
|
| 381 |
+
<div className="space-y-1.5">
|
| 382 |
+
<label className="text-xs text-gray-400 font-medium">Your Feedback</label>
|
| 383 |
+
<textarea
|
| 384 |
+
value={formData.feedback}
|
| 385 |
+
onChange={(e) => setFormData({...formData, feedback: e.target.value})}
|
| 386 |
+
className="w-full bg-black border border-gray-800 rounded-lg p-3 text-sm focus:border-teal-500 outline-none h-40 resize-none"
|
| 387 |
+
placeholder="How can we improve?"
|
| 388 |
+
/>
|
| 389 |
+
</div>
|
| 390 |
+
)}
|
| 391 |
+
|
| 392 |
+
{activeModal === 'context' && (
|
| 393 |
+
<div className="space-y-1.5">
|
| 394 |
+
<label className="text-xs text-gray-400 font-medium">New Context / Fuse Box</label>
|
| 395 |
+
<textarea
|
| 396 |
+
value={formData.context}
|
| 397 |
+
onChange={(e) => setFormData({...formData, context: e.target.value})}
|
| 398 |
+
className="w-full bg-black border border-gray-800 rounded-lg p-3 text-sm focus:border-teal-500 outline-none h-40 resize-none"
|
| 399 |
+
placeholder="Specify the testing environment or scenario..."
|
| 400 |
+
/>
|
| 401 |
+
</div>
|
| 402 |
+
)}
|
| 403 |
+
|
| 404 |
+
{activeModal === 'test' && (
|
| 405 |
+
<>
|
| 406 |
+
<div className="space-y-1.5">
|
| 407 |
+
<label className="text-xs text-gray-400 font-medium">Test Name</label>
|
| 408 |
+
<input
|
| 409 |
+
type="text"
|
| 410 |
+
value={formData.testName}
|
| 411 |
+
onChange={(e) => setFormData({...formData, testName: e.target.value})}
|
| 412 |
+
className="w-full bg-black border border-gray-800 rounded-lg p-3 text-sm focus:border-teal-500 outline-none"
|
| 413 |
+
placeholder="Campaign Launch 2024..."
|
| 414 |
+
/>
|
| 415 |
+
</div>
|
| 416 |
+
<div className="space-y-1.5">
|
| 417 |
+
<label className="text-xs text-gray-400 font-medium">Brand Asset for Testing</label>
|
| 418 |
+
<div className="flex items-center justify-center w-full">
|
| 419 |
+
<label className="flex flex-col items-center justify-center w-full h-32 border-2 border-gray-800 border-dashed rounded-lg cursor-pointer bg-black hover:bg-gray-900 transition-colors">
|
| 420 |
+
<div className="flex flex-col items-center justify-center pt-5 pb-6">
|
| 421 |
+
<Plus className="w-8 h-8 mb-4 text-gray-500" />
|
| 422 |
+
<p className="mb-2 text-sm text-gray-500"><span className="font-semibold">Click to upload</span> or drag and drop</p>
|
| 423 |
+
<p className="text-xs text-gray-500">SVG, PNG, JPG (MAX. 800x400px)</p>
|
| 424 |
+
</div>
|
| 425 |
+
<input type="file" className="hidden" multiple accept="image/*" />
|
| 426 |
+
</label>
|
| 427 |
+
</div>
|
| 428 |
+
</div>
|
| 429 |
+
</>
|
| 430 |
+
)}
|
| 431 |
+
</div>
|
| 432 |
+
|
| 433 |
+
<div className="p-6 border-t border-gray-800 flex gap-3">
|
| 434 |
+
<button
|
| 435 |
+
onClick={() => setActiveModal('none')}
|
| 436 |
+
className="flex-1 py-2.5 rounded-xl border border-gray-800 text-sm font-medium hover:bg-gray-900 transition-colors"
|
| 437 |
+
>
|
| 438 |
+
Cancel
|
| 439 |
+
</button>
|
| 440 |
+
<button
|
| 441 |
+
onClick={async () => {
|
| 442 |
+
// Save to backend logic
|
| 443 |
+
try {
|
| 444 |
+
const response = await fetch('/api/save-data', {
|
| 445 |
+
method: 'POST',
|
| 446 |
+
headers: { 'Content-Type': 'application/json' },
|
| 447 |
+
body: JSON.stringify({
|
| 448 |
+
type: activeModal,
|
| 449 |
+
data: formData,
|
| 450 |
+
user: user?.preferred_username || 'anonymous'
|
| 451 |
+
})
|
| 452 |
+
});
|
| 453 |
+
if (response.ok) {
|
| 454 |
+
alert('Successfully saved!');
|
| 455 |
+
setActiveModal('none');
|
| 456 |
+
}
|
| 457 |
+
} catch (e) {
|
| 458 |
+
console.error(e);
|
| 459 |
+
alert('Successfully submitted (Local simulation)');
|
| 460 |
+
setActiveModal('none');
|
| 461 |
+
}
|
| 462 |
+
}}
|
| 463 |
+
className="flex-1 py-2.5 rounded-xl bg-teal-600 text-white text-sm font-bold hover:bg-teal-500 transition-colors shadow-lg shadow-teal-900/20"
|
| 464 |
+
>
|
| 465 |
+
Confirm
|
| 466 |
+
</button>
|
| 467 |
+
</div>
|
| 468 |
+
</div>
|
| 469 |
+
</div>
|
| 470 |
+
)}
|
| 471 |
+
|
| 472 |
{/* Floating Chat Button (Bottom) */}
|
| 473 |
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 z-30">
|
| 474 |
<button
|
package-lock.json
CHANGED
|
@@ -22,6 +22,7 @@
|
|
| 22 |
"vite-plugin-node-polyfills": "^0.25.0"
|
| 23 |
},
|
| 24 |
"devDependencies": {
|
|
|
|
| 25 |
"@types/node": "^22.14.0",
|
| 26 |
"@vitejs/plugin-react": "^5.0.0",
|
| 27 |
"typescript": "~5.8.2",
|
|
@@ -895,6 +896,22 @@
|
|
| 895 |
"node": ">=6.0.0"
|
| 896 |
}
|
| 897 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 898 |
"node_modules/@plotly/d3": {
|
| 899 |
"version": "3.8.1",
|
| 900 |
"resolved": "https://registry.npmjs.org/@plotly/d3/-/d3-3.8.1.tgz",
|
|
|
|
| 22 |
"vite-plugin-node-polyfills": "^0.25.0"
|
| 23 |
},
|
| 24 |
"devDependencies": {
|
| 25 |
+
"@playwright/test": "^1.58.2",
|
| 26 |
"@types/node": "^22.14.0",
|
| 27 |
"@vitejs/plugin-react": "^5.0.0",
|
| 28 |
"typescript": "~5.8.2",
|
|
|
|
| 896 |
"node": ">=6.0.0"
|
| 897 |
}
|
| 898 |
},
|
| 899 |
+
"node_modules/@playwright/test": {
|
| 900 |
+
"version": "1.58.2",
|
| 901 |
+
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
|
| 902 |
+
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
|
| 903 |
+
"dev": true,
|
| 904 |
+
"license": "Apache-2.0",
|
| 905 |
+
"dependencies": {
|
| 906 |
+
"playwright": "1.58.2"
|
| 907 |
+
},
|
| 908 |
+
"bin": {
|
| 909 |
+
"playwright": "cli.js"
|
| 910 |
+
},
|
| 911 |
+
"engines": {
|
| 912 |
+
"node": ">=18"
|
| 913 |
+
}
|
| 914 |
+
},
|
| 915 |
"node_modules/@plotly/d3": {
|
| 916 |
"version": "3.8.1",
|
| 917 |
"resolved": "https://registry.npmjs.org/@plotly/d3/-/d3-3.8.1.tgz",
|
package.json
CHANGED
|
@@ -23,6 +23,7 @@
|
|
| 23 |
"vite-plugin-node-polyfills": "^0.25.0"
|
| 24 |
},
|
| 25 |
"devDependencies": {
|
|
|
|
| 26 |
"@types/node": "^22.14.0",
|
| 27 |
"@vitejs/plugin-react": "^5.0.0",
|
| 28 |
"typescript": "~5.8.2",
|
|
|
|
| 23 |
"vite-plugin-node-polyfills": "^0.25.0"
|
| 24 |
},
|
| 25 |
"devDependencies": {
|
| 26 |
+
"@playwright/test": "^1.58.2",
|
| 27 |
"@types/node": "^22.14.0",
|
| 28 |
"@vitejs/plugin-react": "^5.0.0",
|
| 29 |
"typescript": "~5.8.2",
|
server.cjs
CHANGED
|
@@ -2,10 +2,11 @@ const express = require('express');
|
|
| 2 |
const path = require('path');
|
| 3 |
const cookieParser = require('cookie-parser');
|
| 4 |
const crypto = require('crypto');
|
|
|
|
| 5 |
const app = express();
|
| 6 |
const port = 7860;
|
| 7 |
|
| 8 |
-
app.use(express.json());
|
| 9 |
app.use(cookieParser());
|
| 10 |
app.use(express.static(path.join(__dirname, 'dist')));
|
| 11 |
|
|
@@ -153,6 +154,57 @@ app.get('/api/logout', (req, res) => {
|
|
| 153 |
res.redirect('/');
|
| 154 |
});
|
| 155 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
app.get('/health', (req, res) => {
|
| 157 |
res.status(200).send('OK');
|
| 158 |
});
|
|
|
|
| 2 |
const path = require('path');
|
| 3 |
const cookieParser = require('cookie-parser');
|
| 4 |
const crypto = require('crypto');
|
| 5 |
+
const fs = require('fs');
|
| 6 |
const app = express();
|
| 7 |
const port = 7860;
|
| 8 |
|
| 9 |
+
app.use(express.json({ limit: '50mb' }));
|
| 10 |
app.use(cookieParser());
|
| 11 |
app.use(express.static(path.join(__dirname, 'dist')));
|
| 12 |
|
|
|
|
| 154 |
res.redirect('/');
|
| 155 |
});
|
| 156 |
|
| 157 |
+
app.post('/api/save-data', (req, res) => {
|
| 158 |
+
const { type, data, user } = req.body;
|
| 159 |
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
| 160 |
+
const filename = `${user}_${type}_${timestamp}.json`;
|
| 161 |
+
const dirPath = path.join(__dirname, 'data');
|
| 162 |
+
|
| 163 |
+
if (!fs.existsSync(dirPath)) {
|
| 164 |
+
fs.mkdirSync(dirPath, { recursive: true });
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
const filePath = path.join(dirPath, filename);
|
| 168 |
+
|
| 169 |
+
try {
|
| 170 |
+
fs.writeFileSync(filePath, JSON.stringify({ user, type, timestamp, data }, null, 2));
|
| 171 |
+
console.log(`Saved data to ${filePath}`);
|
| 172 |
+
res.json({ success: true, message: `Data saved as ${filename}` });
|
| 173 |
+
} catch (error) {
|
| 174 |
+
console.error('Failed to save data:', error);
|
| 175 |
+
res.status(500).json({ error: 'Failed to save data' });
|
| 176 |
+
}
|
| 177 |
+
});
|
| 178 |
+
|
| 179 |
+
app.get('/api/list-data', (req, res) => {
|
| 180 |
+
const { type, user } = req.query;
|
| 181 |
+
const dirPath = path.join(__dirname, 'data');
|
| 182 |
+
|
| 183 |
+
if (!fs.existsSync(dirPath)) {
|
| 184 |
+
return res.json([]);
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
try {
|
| 188 |
+
const files = fs.readdirSync(dirPath);
|
| 189 |
+
const results = files
|
| 190 |
+
.filter(f => f.endsWith('.json'))
|
| 191 |
+
.map(f => {
|
| 192 |
+
try {
|
| 193 |
+
const content = fs.readFileSync(path.join(dirPath, f), 'utf8');
|
| 194 |
+
return JSON.parse(content);
|
| 195 |
+
} catch (e) {
|
| 196 |
+
return null;
|
| 197 |
+
}
|
| 198 |
+
})
|
| 199 |
+
.filter(d => d && (!type || d.type === type) && (!user || d.user === user));
|
| 200 |
+
|
| 201 |
+
res.json(results);
|
| 202 |
+
} catch (error) {
|
| 203 |
+
console.error('Failed to list data:', error);
|
| 204 |
+
res.status(500).json({ error: 'Failed to list data' });
|
| 205 |
+
}
|
| 206 |
+
});
|
| 207 |
+
|
| 208 |
app.get('/health', (req, res) => {
|
| 209 |
res.status(200).send('OK');
|
| 210 |
});
|
test-results/.last-run.json
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"status": "passed",
|
| 3 |
+
"failedTests": []
|
| 4 |
+
}
|
verify_features.spec.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import { test, expect } from '@playwright/test';
|
| 3 |
+
|
| 4 |
+
test('Verify app features', async ({ page }) => {
|
| 5 |
+
await page.goto('http://localhost:7860');
|
| 6 |
+
|
| 7 |
+
// Wait for the app to load
|
| 8 |
+
await page.waitForSelector('text=Branding Content Testing');
|
| 9 |
+
|
| 10 |
+
// 1. Verify Default View is Job Title
|
| 11 |
+
// The first select is Focus Group, the second is Current View
|
| 12 |
+
const currentView = await page.locator('select').nth(1).inputValue();
|
| 13 |
+
console.log('Current View:', currentView);
|
| 14 |
+
expect(currentView).toBe('Job Title');
|
| 15 |
+
|
| 16 |
+
// 2. Verify Info Box
|
| 17 |
+
await expect(page.locator('text=Configuration Required')).toBeVisible();
|
| 18 |
+
await expect(page.locator('text=Assemble new group and create a new test are required')).toBeVisible();
|
| 19 |
+
|
| 20 |
+
// 3. Verify Output Panel
|
| 21 |
+
await expect(page.locator('text=OUTPUT')).toBeVisible();
|
| 22 |
+
await expect(page.locator('text=Simulation Results')).toBeVisible();
|
| 23 |
+
|
| 24 |
+
// 4. Test "Assemble new group" modal
|
| 25 |
+
await page.click('text=Assemble new group');
|
| 26 |
+
await expect(page.locator('h3:has-text("Assemble New Group")')).toBeVisible();
|
| 27 |
+
await page.fill('textarea[placeholder="Describe your ideal audience..."]', 'Test Profile');
|
| 28 |
+
await page.click('button:has-text("Cancel")');
|
| 29 |
+
|
| 30 |
+
// 5. Test Chat Page
|
| 31 |
+
await page.click('text=Open Global Chat');
|
| 32 |
+
await expect(page.locator('text=New Simulation')).toBeVisible();
|
| 33 |
+
|
| 34 |
+
// 6. Verify Help Me Craft button
|
| 35 |
+
await expect(page.locator('button:has-text("Help Me Craft")')).toBeVisible();
|
| 36 |
+
|
| 37 |
+
// 7. Verify Upload Images button
|
| 38 |
+
await expect(page.locator('button:has-text("Upload Images")')).toBeVisible();
|
| 39 |
+
|
| 40 |
+
// 8. Test "Request a new context" button in Chat
|
| 41 |
+
await page.click('text=Request a new context');
|
| 42 |
+
await expect(page.locator('h3:has-text("Request New Context")')).toBeVisible();
|
| 43 |
+
await page.click('button:has-text("Cancel")');
|
| 44 |
+
|
| 45 |
+
// 9. Verify /health endpoint
|
| 46 |
+
const response = await page.request.get('http://localhost:7860/health');
|
| 47 |
+
expect(response.status()).toBe(200);
|
| 48 |
+
console.log('/health status:', response.status());
|
| 49 |
+
|
| 50 |
+
// Take a screenshot of the chat page
|
| 51 |
+
await page.screenshot({ path: 'chat_page_check.png', fullPage: true });
|
| 52 |
+
|
| 53 |
+
console.log('Verification completed successfully');
|
| 54 |
+
});
|