Compare commits

..

2 Commits

Author SHA1 Message Date
Flatlogic Bot
2e3424ad5c v.2 2025-11-02 19:42:55 +00:00
Flatlogic Bot
f521b6df27 v.1 2025-11-02 19:31:04 +00:00
9 changed files with 984 additions and 154 deletions

104
api/analyze_chat.php Normal file
View File

@ -0,0 +1,104 @@
<?php
// api/analyze_chat.php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/config.php';
header('Content-Type: application/json');
$json = file_get_contents('php://input');
$data = json_decode($json, true);
if (!isset($data['session_id'], $data['persona'], $data['history'])) {
http_response_code(400);
echo json_encode(['error' => 'Invalid input. session_id, persona, and history are required.']);
exit;
}
$sessionId = $data['session_id'];
$persona = $data['persona'];
$history = $data['history'];
// --- System Prompt & API Call (remains the same) ---
$system_prompt = "You are a market research analyst. Your task is to analyze a conversation between an entrepreneur and a potential customer persona. The persona's details and the conversation are provided below.\n\n"
. "**Persona Profile:**\n"
. "- Name: {$persona['name']}\n"
. "- Age: {$persona['age']}\n"
. "- Occupation: {$persona['occupation']}\n"
. "- Traits: {$persona['traits']}\n"
. "- Concerns: {$persona['concerns']}\n"
. "- Style: {$persona['style']}\n\n"
. "**Your Analysis Should Include:**\n"
. "1. **Key Takeaways:** A bulleted list of the most important points from the conversation.\n"
. "2. **Persona's Sentiment:** A brief assessment (Positive, Negative, Neutral, Mixed) of the persona's overall feeling about the business idea, with a brief justification.\n"
. "3. **Actionable Insights:** A bulleted list of concrete suggestions for the entrepreneur to improve their idea based on the persona's feedback.\n\n"
. "Please format your response in simple HTML, using <h3> for titles and <ul> and <li> for lists. Do not include <html>, <head>, or <body> tags.";
$conversation_text = "";
foreach ($history as $msg) {
$sender = $msg['sender'] === 'user' ? 'Entrepreneur' : $persona['name'];
$conversation_text .= "**{$sender}:** {$msg['message']}\n";
}
$user_content = "Please analyze the following conversation:\n\n{$conversation_text}";
$messages = [[ 'role' => 'user', 'content' => $user_content ]];
$apiKey = defined('ANTHROPIC_API_KEY') ? ANTHROPIC_API_KEY : '';
if (empty($apiKey) || $apiKey === 'YOUR_ANTHROPIC_API_KEY') {
http_response_code(500);
echo json_encode(['error' => 'Anthropic API key is not configured.']);
exit;
}
$payload = [
'model' => 'claude-3-sonnet-20240229',
'system' => $system_prompt,
'messages' => $messages,
'max_tokens' => 1024,
'temperature' => 0.5,
];
$ch = curl_init('https://api.anthropic.com/v1/messages');
// ... (cURL options remain the same)
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'x-api-key: ' . $apiKey,
'anthropic-version: 2023-06-01'
]);
$response = curl_exec($ch);
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_error($ch);
curl_close($ch);
if ($curl_error || $httpcode >= 400) {
http_response_code($httpcode > 0 ? $httpcode : 500);
$error_details = json_decode($response, true);
echo json_encode(['error' => 'Anthropic API Error', 'details' => $error_details, 'curl_error' => $curl_error]);
exit;
}
$result = json_decode($response, true);
$analysisContent = $result['content'][0]['text'] ?? null;
if ($analysisContent) {
// --- Save analysis to DB ---
try {
$pdo = db();
// Use INSERT ... ON DUPLICATE KEY UPDATE to avoid creating multiple analyses for one session.
// Note: This requires a UNIQUE index on session_id in the analyses table.
// For simplicity here, we'll just insert. A more robust app would handle this better.
$stmt = $pdo->prepare("INSERT INTO analyses (session_id, content) VALUES (?, ?)");
$stmt->execute([$sessionId, $analysisContent]);
} catch (Exception $e) {
error_log('Database error while saving analysis: ' . $e->getMessage());
}
echo json_encode(['analysis' => $analysisContent]);
} else {
http_response_code(500);
echo json_encode(['error' => 'Unexpected API response structure for analysis.', 'details' => $result]);
}

17
api/chat.php Normal file
View File

@ -0,0 +1,17 @@
<?php
header('Content-Type: application/json');
// Simulate a delay
sleep(1);
// Get the message from the user
$input = json_decode(file_get_contents('php://input'), true);
$message = $input['message'] ?? 'No message received';
$persona_name = $input['persona']['name'] ?? 'Persona';
// A simple, canned response for now
$response = "This is a placeholder response from " . htmlspecialchars($persona_name) . ". You said: '" . htmlspecialchars($message) . "'. The real AI chat functionality will be implemented later.";
echo json_encode([
'reply' => $response
]);

6
api/config.php Normal file
View File

@ -0,0 +1,6 @@
<?php
// api/config.php
// IMPORTANT: Replace with your actual Anthropic API key.
// You can get a key from https://console.anthropic.com/
define('ANTHROPIC_API_KEY', 'YOUR_ANTHROPIC_API_KEY_HERE');

135
api/generate_personas.php Normal file
View File

@ -0,0 +1,135 @@
<?php
// api/generate_personas.php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/config.php';
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Method Not Allowed']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
$audience = $input['audience'] ?? '';
$idea = $input['idea'] ?? '';
if (empty($audience) || empty($idea)) {
http_response_code(400);
echo json_encode(['error' => 'Audience and Idea are required.']);
exit;
}
if (ANTHROPIC_API_KEY === 'YOUR_ANTHROPIC_API_KEY_HERE') {
http_response_code(500);
echo json_encode(['error' => 'Anthropic API key is not configured. Please add it to api/config.php.']);
exit;
}
// ... (Anthropic API call logic remains the same)
$prompt = "Based on the following business idea and target audience in Egypt/MENA, create 3 distinct user personas.
Target Audience: "$audience"
Business Idea: "$idea"
For each persona, provide:
- A culturally relevant Egyptian/MENA name.
- Age and a realistic occupation in the Egyptian market.
- 2-3 key personality traits.
- Main concerns and priorities relevant to the Egyptian/MENA context.
- A typical communication style.
Please return the response as a JSON object with a single key 'personas' which is an array of the 3 persona objects. Do not include any other text or explanation outside of the JSON object.
Each persona object in the array should have the following keys: 'name', 'age', 'occupation', 'traits', 'concerns', 'style'.";
$data = [
'model' => 'claude-3-sonnet-20240229',
'max_tokens' => 2048,
'messages' => [
[
'role' => 'user',
'content' => $prompt,
],
],
];
$ch = curl_init('https://api.anthropic.com/v1/messages');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'x-api-key: ' . ANTHROPIC_API_KEY,
'anthropic-version: 2023-06-01',
'content-type: application/json',
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code !== 200) {
http_response_code(502);
echo json_encode(['error' => 'Failed to communicate with AI service.', 'details' => json_decode($response)]);
exit;
}
$result = json_decode($response, true);
$content_json = $result['content'][0]['text'] ?? null;
if (!$content_json) {
http_response_code(500);
echo json_encode(['error' => 'Invalid response format from AI service.']);
exit;
}
$personas_data = json_decode($content_json, true);
if (json_last_error() !== JSON_ERROR_NONE || !isset($personas_data['personas'])) {
http_response_code(500);
echo json_encode(['error' => 'Could not parse personas from AI response.', 'raw_content' => $content_json]);
exit;
}
// --- Database Integration ---
try {
$pdo = db();
// 1. Create a new session
$stmt = $pdo->prepare("INSERT INTO sessions (target_audience, business_idea) VALUES (?, ?)");
$stmt->execute([$audience, $idea]);
$session_id = $pdo->lastInsertId();
// 2. Save each persona
$saved_personas = [];
$persona_stmt = $pdo->prepare(
"INSERT INTO personas (session_id, name, age, occupation, traits, concerns, style) VALUES (?, ?, ?, ?, ?, ?, ?)"
);
foreach ($personas_data['personas'] as $p) {
$persona_stmt->execute([
$session_id,
$p['name'],
$p['age'],
$p['occupation'],
$p['traits'],
$p['concerns'],
$p['style']
]);
$p['id'] = $pdo->lastInsertId(); // Add the new DB ID to the persona object
$saved_personas[] = $p;
}
// 3. Return the session ID and the updated persona data
echo json_encode([
'session_id' => $session_id,
'personas' => $saved_personas
]);
} catch (Exception $e) {
http_response_code(500);
// In a real app, log this error instead of echoing it.
echo json_encode(['error' => 'Database error while saving session.', 'details' => $e->getMessage()]);
exit;
}

220
assets/css/custom.css Normal file
View File

@ -0,0 +1,220 @@
/* Screen Test - Custom Styles */
body {
font-family: 'Inter', sans-serif;
background: linear-gradient(to bottom, #F9FAFB, #F3F4F6);
}
.main-title {
font-weight: 800; /* Extra Bold */
color: #111827; /* gray-900 */
}
.form-card {
width: 100%;
max-width: 700px;
background-color: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.form-control-lg {
padding: 1rem 1.25rem;
font-size: 1.1rem;
background-color: #F9FAFB;
border: 1px solid #E5E7EB;
}
.form-control-lg:focus {
background-color: #fff;
border-color: #A855F7;
box-shadow: 0 0 0 0.25rem rgba(168, 85, 247, 0.2);
}
.primary-gradient-btn {
background: linear-gradient(45deg, #D946EF, #A855F7, #6366F1);
border: none;
padding: 0.8rem 1.5rem;
transition: all 0.3s ease;
}
.primary-gradient-btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1) !important;
}
/* Shadow Utilities */
.shadow-xl {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.shadow-2xl {
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
/* Rounded Utilities */
.rounded-4 {
border-radius: 1rem !important;
}
.rounded-5 {
border-radius: 1.5rem !important;
}
/* Persona Card Styles */
.persona-card {
color: #fff;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.persona-card:hover {
transform: translateY(-5px);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25) !important;
}
.persona-card .card-title {
color: #fff;
}
.persona-card .card-subtitle {
color: rgba(255, 255, 255, 0.8);
}
.persona-card h6 {
color: rgba(255, 255, 255, 0.9);
text-transform: uppercase;
font-size: 0.8rem;
letter-spacing: 0.5px;
}
.persona-card p {
color: rgba(255, 255, 255, 0.95);
}
/* Persona Gradients */
.persona-gradient-1 {
background: linear-gradient(45deg, #22d3ee, #06b6d4); /* Cyan */
}
.persona-gradient-2 {
background: linear-gradient(45deg, #34d399, #059669); /* Emerald */
}
.persona-gradient-3 {
background: linear-gradient(45deg, #a78bfa, #7c3aed); /* Violet */
}
.text-white-75 {
color: rgba(255, 255, 255, 0.75) !important;
}
/* Chat Interface Styles */
.chat-card {
width: 100%;
max-width: 800px;
height: 70vh;
background-color: #fff;
}
.chat-card .card-header {
background-color: #F9FAFB;
border-bottom: 1px solid #E5E7EB;
}
#chat-log {
overflow-y: auto;
height: calc(70vh - 150px); /* Adjust based on header/footer height */
}
.chat-message {
margin-bottom: 1rem;
display: flex;
flex-direction: column;
}
.chat-message .message-bubble {
padding: 0.75rem 1.25rem;
border-radius: 1.25rem;
max-width: 80%;
word-wrap: break-word;
}
.chat-message.user .message-bubble {
background: linear-gradient(45deg, #D946EF, #A855F7);
color: white;
border-bottom-right-radius: 0.25rem;
align-self: flex-end;
}
.chat-message.persona .message-bubble {
background-color: #E5E7EB;
color: #1F2937;
border-bottom-left-radius: 0.25rem;
align-self: flex-start;
}
.chat-message .message-time {
font-size: 0.75rem;
color: #6B7280;
margin-top: 0.25rem;
}
.chat-message.user .message-time {
align-self: flex-end;
}
.chat-message.persona .message-time {
align-self: flex-start;
}
#chat-message-input {
resize: none;
}
/* Analysis Section Styles */
.analysis-gradient-btn {
background: linear-gradient(45deg, #fbbf24, #f97316); /* Amber to Orange */
border: none;
color: white;
padding: 0.8rem 1.5rem;
transition: all 0.3s ease;
}
.analysis-gradient-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1) !important;
color: white;
}
.analysis-gradient-btn:disabled {
background: #E5E7EB;
cursor: not-allowed;
}
.analysis-card {
width: 100%;
max-width: 800px;
background-color: #FFFAF0; /* Floral White, a very light orange/yellow */
border: 1px solid #FDBA74; /* Light Orange */
}
#analysis-content h3 {
color: #D97706; /* Dark Amber */
font-weight: 700;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
}
#analysis-content ul {
list-style-type: none;
padding-left: 0;
}
#analysis-content li {
background-color: rgba(255, 255, 255, 0.6);
padding: 0.75rem 1.25rem;
border-radius: 0.75rem;
margin-bottom: 0.5rem;
border-left: 5px solid #F97316; /* Orange */
}

289
assets/js/main.js Normal file
View File

@ -0,0 +1,289 @@
// Screen Test - Main JS
document.addEventListener('DOMContentLoaded', function () {
// --- Element Selectors ---
const form = document.getElementById('idea-form');
const submitButton = form.querySelector('button[type="submit"]');
const personasContainer = document.getElementById('personas-container');
const personasGrid = document.getElementById('personas-grid');
const chatContainer = document.getElementById('chat-container');
const chatCard = document.querySelector('.chat-card');
const chatPersonaName = document.getElementById('chat-persona-name');
const chatPersonaIcon = document.getElementById('chat-persona-icon');
const closeChatBtn = document.getElementById('close-chat');
const chatLog = document.getElementById('chat-log');
const chatForm = document.getElementById('chat-form');
const chatMessageInput = document.getElementById('chat-message-input');
const sendMessageBtn = document.getElementById('send-message-btn');
const analysisSection = document.getElementById('analysis-section');
const generateAnalysisBtn = document.getElementById('generate-analysis-btn');
const analysisContainer = document.getElementById('analysis-container');
const analysisContent = document.getElementById('analysis-content');
// --- State Variables ---
let currentSessionId = null;
let personasData = [];
let activePersona = null;
let chatHistory = [];
// --- Event Listeners ---
// Main form submission -> Generate Personas & Create Session
if (form) {
form.addEventListener('submit', function (e) {
e.preventDefault();
const audience = document.getElementById('target-audience').value;
const idea = document.getElementById('business-idea').value;
if (!audience || !idea) {
alert('Please fill out both Target Audience and Business Idea fields.');
return;
}
// --- Reset UI and Set Loading State ---
personasContainer.classList.add('d-none');
chatContainer.classList.add('d-none');
analysisSection.classList.add('d-none');
analysisContainer.classList.add('d-none');
personasGrid.innerHTML = '';
const originalButtonText = submitButton.innerHTML;
submitButton.disabled = true;
submitButton.innerHTML = `<span class="spinner-border spinner-border-sm"></span> Generating...`;
// --- Fetch Personas & Create Session ---
fetch('api/generate_personas.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ audience, idea }),
})
.then(response => {
if (!response.ok) return response.json().then(err => { throw new Error(err.error || 'Unknown error generating personas.') });
return response.json();
})
.then(data => {
if (data.session_id && data.personas && data.personas.length > 0) {
// --- Store Session and Persona Data ---
currentSessionId = data.session_id;
personasData = data.personas; // Now contains DB IDs
renderPersonas(personasData);
personasContainer.classList.remove('d-none');
setTimeout(() => personasContainer.scrollIntoView({ behavior: 'smooth', block: 'start' }), 100);
} else {
throw new Error('The AI did not return valid session data. Please try again.');
}
})
.catch(error => {
console.error('Error:', error);
alert(`An error occurred: ${error.message}`);
})
.finally(() => {
submitButton.disabled = false;
submitButton.innerHTML = originalButtonText;
});
});
}
// Persona card click -> Open Chat
personasGrid.addEventListener('click', function(e) {
const card = e.target.closest('.persona-card');
if (card) {
const personaIndex = card.dataset.personaIndex;
activePersona = personasData[personaIndex]; // activePersona now has the DB ID
const gradientIndex = card.classList.contains('persona-gradient-1') ? 1 : card.classList.contains('persona-gradient-2') ? 2 : 3;
openChat(activePersona, gradientIndex);
}
});
// Close chat button
closeChatBtn.addEventListener('click', function() {
chatContainer.classList.add('d-none');
analysisSection.classList.add('d-none');
analysisContainer.classList.add('d-none');
activePersona = null;
});
// Chat form submission -> Send message
chatForm.addEventListener('submit', function(e) {
e.preventDefault();
const message = chatMessageInput.value.trim();
if (message && activePersona && currentSessionId) {
addMessageToChatLog(message, 'user');
chatHistory.push({ sender: 'user', message: message });
chatMessageInput.value = '';
chatMessageInput.style.height = 'auto';
showTypingIndicator();
// --- Fetch Chat Response ---
fetch('api/chat.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: currentSessionId,
message: message,
persona: activePersona, // persona object now includes the ID
history: chatHistory.slice(0, -1)
}),
})
.then(response => {
if (!response.ok) return response.json().then(err => { throw new Error(err.error || 'An API error occurred.') });
return response.json();
})
.then(data => {
if (data.response) {
addMessageToChatLog(data.response, 'persona');
chatHistory.push({ sender: 'persona', message: data.response });
} else {
throw new Error('Empty response from AI.');
}
})
.catch(error => {
console.error('Chat Error:', error);
addMessageToChatLog(`Sorry, I encountered an error: ${error.message}`, 'persona', true);
})
.finally(() => {
hideTypingIndicator();
});
}
});
// Analysis button click -> Generate Analysis
generateAnalysisBtn.addEventListener('click', function() {
if (!activePersona || !currentSessionId || chatHistory.length < 5) return;
const originalButtonText = this.innerHTML;
this.disabled = true;
this.innerHTML = `<span class="spinner-border spinner-border-sm"></span> Analyzing...`;
// --- Fetch Analysis ---
fetch('api/analyze_chat.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: currentSessionId,
persona: activePersona,
history: chatHistory
}),
})
.then(response => {
if (!response.ok) return response.json().then(err => { throw new Error(err.error || 'An API error occurred during analysis.') });
return response.json();
})
.then(data => {
if (data.analysis) {
analysisContent.innerHTML = data.analysis;
analysisContainer.classList.remove('d-none');
analysisContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
} else {
throw new Error('Empty analysis from AI.');
}
})
.catch(error => {
console.error('Analysis Error:', error);
analysisContent.innerHTML = `<div class="alert alert-danger">Sorry, an error occurred during analysis: ${error.message}</div>`;
analysisContainer.classList.remove('d-none');
})
.finally(() => {
this.disabled = false;
this.innerHTML = originalButtonText;
});
});
// Auto-resize textarea for chat input
chatMessageInput.addEventListener('input', function () {
this.style.height = 'auto';
this.style.height = (this.scrollHeight) + 'px';
});
// --- Functions ---
function renderPersonas(personas) {
const personaGradients = ['persona-gradient-1', 'persona-gradient-2', 'persona-gradient-3'];
personasGrid.innerHTML = personas.map((persona, index) => `
<div class="col-lg-4 col-md-6">
<div class="card persona-card h-100 rounded-4 shadow-lg border-0 ${personaGradients[index % 3]}" data-persona-index="${index}" style="cursor: pointer;">
<div class="card-body p-4">
<h3 class="card-title fw-bold">${persona.name}</h3>
<p class="card-subtitle mb-2 text-white-75">${persona.age}, ${persona.occupation}</p>
<div class="mt-4">
<h6 class="fw-semibold">Traits:</h6><p>${persona.traits}</p>
<h6 class="fw-semibold">Concerns:</h6><p>${persona.concerns}</p>
<h6 class="fw-semibold">Style:</h6><p>${persona.style}</p>
</div>
</div>
</div>
</div>
`).join('');
}
function openChat(persona, gradientIndex) {
chatPersonaName.textContent = persona.name;
const cardGradientClass = `persona-gradient-${gradientIndex}`;
chatCard.querySelector('.card-header').className = `card-header d-flex justify-content-between align-items-center p-3 rounded-top-5 ${cardGradientClass}`;
chatPersonaIcon.className = "bi bi-person-circle fs-3 me-3";
chatLog.innerHTML = '';
const welcomeMessage = `Hello! I'm ${persona.name}. Ask me anything about the business idea.`;
addMessageToChatLog(welcomeMessage, 'persona');
chatHistory = [{ sender: 'persona', message: welcomeMessage }];
chatContainer.classList.remove('d-none');
analysisSection.classList.add('d-none');
analysisContainer.classList.add('d-none');
generateAnalysisBtn.disabled = true;
chatContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function addMessageToChatLog(message, sender, isError = false) {
const messageElement = document.createElement('div');
messageElement.className = `chat-message ${sender === 'user' ? 'user' : 'persona'}`;
const bubble = document.createElement('div');
bubble.className = 'message-bubble';
if (isError) bubble.classList.add('bg-danger', 'text-white');
bubble.textContent = message;
const time = document.createElement('div');
time.className = 'message-time';
time.textContent = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
messageElement.appendChild(bubble);
messageElement.appendChild(time);
chatLog.appendChild(messageElement);
chatLog.scrollTop = chatLog.scrollHeight;
// Check to enable analysis button (user messages count towards total)
if (sender === 'user' && chatHistory.filter(m => m.sender === 'user').length >= 2) {
analysisSection.classList.remove('d-none');
generateAnalysisBtn.disabled = false;
}
}
function showTypingIndicator() {
sendMessageBtn.disabled = true;
let indicator = chatLog.querySelector('.typing-indicator');
if (!indicator) {
indicator = document.createElement('div');
indicator.className = 'chat-message persona typing-indicator';
indicator.innerHTML = `
<div class="message-bubble">
<div class="spinner-grow spinner-grow-sm" role="status"></div>
<div class="spinner-grow spinner-grow-sm mx-1" role="status"></div>
<div class="spinner-grow spinner-grow-sm" role="status"></div>
</div>
`;
chatLog.appendChild(indicator);
}
chatLog.scrollTop = chatLog.scrollHeight;
}
function hideTypingIndicator() {
sendMessageBtn.disabled = false;
const indicator = chatLog.querySelector('.typing-indicator');
if (indicator) {
indicator.remove();
}
}
});

View File

@ -1,17 +1,55 @@
<?php <?php
// Generated by setup_mariadb_project.sh — edit as needed. // Database Configuration for Supabase (PostgreSQL)
define('DB_HOST', '127.0.0.1');
define('DB_NAME', 'app_31009');
define('DB_USER', 'app_31009');
define('DB_PASS', '2c66b530-2a65-423a-a106-6760b49ad1a2');
// IMPORTANT: Replace with your actual Supabase credentials.
// You can find these in your Supabase project's Database settings.
define('DB_HOST', 'YOUR_SUPABASE_HOST');
define('DB_PORT', '5432'); // Default PostgreSQL port
define('DB_NAME', 'postgres');
define('DB_USER', 'postgres');
define('DB_PASS', 'YOUR_SUPABASE_PASSWORD');
/**
* Establishes a PDO database connection.
* @return PDO
*/
function db() { function db() {
static $pdo; static $pdo;
if (!$pdo) { if (!$pdo) {
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [ $dsn = 'pgsql:host=' . DB_HOST . ';port=' . DB_PORT . ';dbname=' . DB_NAME;
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, try {
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, $pdo = new PDO($dsn, DB_USER, DB_PASS, [
]); PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
} catch (PDOException $e) {
// In a real app, you'd log this error and show a generic message.
// For now, we'll just die and show the error.
die("Database connection failed: " . $e->getMessage());
}
} }
return $pdo; return $pdo;
} }
// A simple function to run migrations.
// In a real application, you would use a more robust migration tool.
function run_migrations() {
$pdo = db();
$migration_dir = __DIR__ . '/migrations';
$files = glob($migration_dir . '/*.sql');
foreach ($files as $file) {
try {
$sql = file_get_contents($file);
$pdo->exec($sql);
} catch (Exception $e) {
// Log error or handle it
error_log("Failed to run migration: $file. Error: " . $e->getMessage());
}
}
}
// Automatically run migrations when this file is included.
// This is for simplicity in this development environment.
if (DB_HOST !== 'YOUR_SUPABASE_HOST') { // Don't run if not configured
run_migrations();
}

View File

@ -0,0 +1,45 @@
-- 001_initial_schema.sql
-- This script creates the initial database schema for Screen Test.
-- Create sessions table to store the core user inputs.
CREATE TABLE IF NOT EXISTS sessions (
id INT AUTO_INCREMENT PRIMARY KEY,
target_audience TEXT NOT NULL,
business_idea TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Create personas table to store the generated AI personas for each session.
CREATE TABLE IF NOT EXISTS personas (
id INT AUTO_INCREMENT PRIMARY KEY,
session_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
age INT,
occupation VARCHAR(255),
traits TEXT,
concerns TEXT,
style TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Create chat_messages table to store the conversation history.
CREATE TABLE IF NOT EXISTS chat_messages (
id INT AUTO_INCREMENT PRIMARY KEY,
session_id INT NOT NULL,
persona_id INT NOT NULL,
sender ENUM('user', 'persona', 'system') NOT NULL,
message TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE,
FOREIGN KEY (persona_id) REFERENCES personas(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Create analyses table to store the generated conversation analysis.
CREATE TABLE IF NOT EXISTS analyses (
id INT AUTO_INCREMENT PRIMARY KEY,
session_id INT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

264
index.php
View File

@ -1,150 +1,126 @@
<?php <!DOCTYPE html>
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
?>
<!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>New Style</title>
<?php <!-- SEO & Meta Tags -->
// Read project preview data from environment <title>Screen Test</title>
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? ''; <meta name="description" content="AI-powered validation for your business ideas in the Egyptian/MENA market.">
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; <meta name="keywords" content="AI business validation, Egypt entrepreneurs, MENA market research, AI personas, startup ideas Egypt, customer feedback simulator, business idea testing, Arabic AI chat, market validation tool, Egyptian consumers, MENA startups, Built with Flatlogic Generator">
?>
<?php if ($projectDescription): ?> <!-- Social Media Meta Tags -->
<!-- Meta description --> <meta property="og:title" content="Screen Test">
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' /> <meta property="og:description" content="AI-powered validation for your business ideas in the Egyptian/MENA market.">
<!-- Open Graph meta tags --> <meta property="og:image" content="<?php echo htmlspecialchars($_SERVER['PROJECT_IMAGE_URL'] ?? ''); ?>">
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" /> <meta name="twitter:card" content="summary_large_image">
<!-- Twitter meta tags --> <meta name="twitter:image" content="<?php echo htmlspecialchars($_SERVER['PROJECT_IMAGE_URL'] ?? ''); ?>">
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?> <!-- Bootstrap CSS -->
<?php if ($projectImageUrl): ?> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<!-- Open Graph image --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image --> <!-- Google Fonts -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" /> <link rel="preconnect" href="https://fonts.googleapis.com">
<?php endif; ?> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet"> <!-- Custom CSS -->
<style> <link rel="stylesheet" href="assets/css/custom.css">
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
</head> </head>
<body> <body>
<main>
<div class="card"> <main class="d-flex flex-column align-items-center justify-content-center min-vh-100 p-3 p-md-4">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes"> <div class="text-center mb-4">
<span class="sr-only">Loading…</span> <h1 class="display-4 fw-bolder main-title">Screen Test</h1>
</div> <p class="lead text-muted">Validate your business idea with AI-powered personas for the Egyptian market.</p>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p> </div>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p> <div class="card form-card rounded-5 shadow-xl border-0">
</div> <div class="card-body p-4 p-lg-5">
</main>
<footer> <form id="idea-form">
Page updated: <?= htmlspecialchars($now) ?> (UTC) <div class="mb-4">
</footer> <label for="target-audience" class="form-label fs-5 fw-semibold">Target Audience</label>
<textarea class="form-control form-control-lg rounded-4" id="target-audience" rows="4" placeholder="e.g., Young professionals in Cairo, aged 25-35, interested in sustainable products..."></textarea>
</div>
<div class="mb-4">
<label for="business-idea" class="form-label fs-5 fw-semibold">Business Idea</label>
<textarea class="form-control form-control-lg rounded-4" id="business-idea" rows="6" placeholder="e.g., A subscription box for locally-sourced organic snacks, delivered monthly..."></textarea>
</div>
<div class="d-grid mt-5">
<button type="submit" class="btn btn-primary btn-lg rounded-4 fw-bold primary-gradient-btn shadow-lg">
Generate Test Personas <i class="bi bi-arrow-right-circle ms-2"></i>
</button>
</div>
</form>
</div>
</div>
<div id="personas-container" class="container mt-5 d-none">
<h2 class="text-center mb-4 fw-bolder">Generated Personas</h2>
<div id="personas-grid" class="row g-4">
<!-- Persona cards will be injected here by JavaScript -->
</div>
</div>
<!-- Chat Interface -->
<div id="chat-container" class="container mt-5 d-none">
<div class="card chat-card rounded-5 shadow-xl border-0">
<div class="card-header d-flex justify-content-between align-items-center p-3 rounded-top-5">
<div class="d-flex align-items-center">
<i id="chat-persona-icon" class="bi bi-person-circle fs-3 me-3"></i>
<h3 id="chat-persona-name" class="fw-bold mb-0"></h3>
</div>
<button id="close-chat" type="button" class="btn-close" aria-label="Close"></button>
</div>
<div class="card-body p-4" id="chat-log">
<!-- Chat messages will be appended here -->
</div>
<div class="card-footer p-3 border-top-0">
<form id="chat-form">
<div class="input-group">
<textarea id="chat-message-input" class="form-control form-control-lg" placeholder="Type your message..." rows="1"></textarea>
<button class="btn btn-primary primary-gradient-btn" type="submit" id="send-message-btn">
<i class="bi bi-send"></i>
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Analysis Section -->
<div id="analysis-section" class="container mt-4 text-center d-none">
<button id="generate-analysis-btn" class="btn btn-lg rounded-4 fw-bold analysis-gradient-btn shadow-lg" disabled>
<i class="bi bi-bar-chart-line-fill me-2"></i> Generate Analysis
</button>
</div>
<div id="analysis-container" class="container mt-5 d-none">
<div class="card analysis-card rounded-5 shadow-xl border-0">
<div class="card-body p-4 p-lg-5">
<h2 class="text-center mb-4 fw-bolder">Conversation Analysis</h2>
<div id="analysis-content">
<!-- Analysis content will be injected here -->
</div>
</div>
</div>
</div>
<footer class="mt-5 text-center text-muted">
<p>Built with <a href="https://flatlogic.com" class="text-decoration-none">Flatlogic</a></p>
</footer>
</main>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<!-- Custom JS -->
<script src="assets/js/main.js"></script>
</body> </body>
</html> </html>