Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
APP_ENV=local
APP_DEBUG=true
LOG_CHANNEL=single

OPENAI_API_KEY=MOCK_MODE
OPENAI_MODEL=gpt-4o-mini

MAIL_TRANSPORT=file
MAIL_FROM_ADDRESS=noreply@example.test
MAIL_FROM_NAME="AI Reply Bot"

DB_CONNECTION=none
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: CI

on:
push:
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
- run: cp .env.example .env
- run: composer install --no-interaction --prefer-dist
- run: composer test
29 changes: 29 additions & 0 deletions .github/workflows/debug-artifact.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: debug-artifact

on:
push:
pull_request:

jobs:
debug:
runs-on: ubuntu-latest
env:
OPENAI_API_KEY: MOCK_MODE
MAIL_TRANSPORT: file
DB_CONNECTION: none
INSTALLER_MOCK: 'true'
steps:
- uses: actions/checkout@v4
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- run: cp .env.example .env
- run: composer install --no-interaction --no-progress
- run: composer run test:pack
- name: Upload debug artifact
uses: actions/upload-artifact@v4
with:
name: debug-${{ github.run_number }}
path: artifacts/debug-*.zip
if-no-files-found: error
17 changes: 13 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
# Ignore vendor and environment files
/vendor/
# Ignore environment files
/.env
/.DS_Store

# Ignore logs and cache
logs/
# Ignore logs and artifacts
storage/logs/*
!storage/logs/.gitkeep
storage/mail/*
!storage/mail/.gitkeep
storage/mock_env/*
!storage/mock_env/.gitkeep
artifacts/*
!artifacts/.gitkeep
*.log
*.eml
*.error_log
/vendor/

# Ignore test output
/phpunit.xml
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ReplyPilot AI

**ReplyPilot AI** is a lightweight, modular PHP application that turns your contact form into an intelligent support responder — powered by OpenAI and integrated with optional license validation.
**ReplyPilot AI** is a lightweight, modular PHP application that turns your contact form into an intelligent support responder — powered by OpenAI and integrated with optional license validation via 3rd party Envato Market license API.

The system currently integrates the **Envato Market license API** and is designed to be easily extended to support other license APIs, AI models, and future use cases. It automatically replies to user messages in a custom tone (e.g., Friendly, Professional), categorizes submissions (Sales, Support, Spam), and stores everything securely in a MySQL database.

Expand Down
2 changes: 2 additions & 0 deletions admin/.htaccess
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

# Placeholder: you can add BasicAuth here later
6 changes: 6 additions & 0 deletions admin/assets/css/admin.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

table{width:100%;border-collapse:collapse}
th,td{padding:8px 6px;border:1px solid #ddd;font-size:14px}
th{background:#f1f5f9}
body{font-family:Arial,Helvetica,sans-serif;margin:0;padding:16px;background:#f7f8fa}
h1{font-size:22px}
Empty file added admin/assets/js/admin.js
Empty file.
10 changes: 10 additions & 0 deletions admin/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

<?php
require __DIR__.'/../bootstrap.php';
use App\Repository\SubmissionRepository;

$db = $GLOBALS['container']['db'];
$stmt = $db->query('SELECT * FROM submissions ORDER BY id DESC LIMIT 100');
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
include __DIR__.'/views/submissions-table.php';
?>
13 changes: 13 additions & 0 deletions admin/views/layout.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Admin - AI Responder</title>
<link rel="stylesheet" href="assets/css/admin.css">
</head>
<body>
<h1>Admin Dashboard</h1>
<?= $content ?>
</body>
</html>
22 changes: 22 additions & 0 deletions admin/views/submissions-table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

<?php ob_start(); ?>
<table>
<thead>
<tr>
<th>ID</th><th>Name</th><th>Email</th><th>Category</th><th>Product</th><th>Date</th>
</tr>
</thead>
<tbody>
<?php foreach($rows as $r): ?>
<tr>
<td><?= $r['id'] ?></td>
<td><?= htmlspecialchars($r['name']) ?></td>
<td><?= htmlspecialchars($r['email']) ?></td>
<td><?= htmlspecialchars($r['category']) ?></td>
<td><?= htmlspecialchars($r['product_name']) ?></td>
<td><?= $r['created_at'] ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php $content = ob_get_clean(); include __DIR__.'/layout.php'; ?>
Binary file removed ai-responder-admin.zip
Binary file not shown.
Binary file removed ai-responder-backend.zip
Binary file not shown.
Binary file removed ai-responder-public.zip
Binary file not shown.
41 changes: 41 additions & 0 deletions app/Core/Env.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php
namespace App\Core;

use Dotenv\Dotenv;

class Env {
protected static bool $loaded = false;
protected static array $defaults = [
'APP_ENV' => 'local',
'APP_DEBUG' => 'true',
'LOG_CHANNEL' => 'single',
'OPENAI_API_KEY' => 'MOCK_MODE',
'OPENAI_MODEL' => 'gpt-4o-mini',
'MAIL_TRANSPORT' => 'file',
'MAIL_FROM_ADDRESS' => 'noreply@example.test',
'MAIL_FROM_NAME' => 'AI Reply Bot',
'DB_CONNECTION' => 'none',
];

public static function load(string $path): void
{
if (self::$loaded) {
return;
}
if (is_file($path)) {
Dotenv::createImmutable(dirname($path))->safeLoad();
}
foreach (self::$defaults as $key => $value) {
if (getenv($key) === false) {
$_ENV[$key] = $value;
putenv("{$key}={$value}");
}
}
self::$loaded = true;
}

public static function get(string $key, $default = null)
{
return $_ENV[$key] ?? getenv($key) ?? $default;
}
}
10 changes: 10 additions & 0 deletions app/Core/Request.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php
namespace App\Core;

class Request {
public static function input($key, $default = null)
{
return $_POST[$key] ?? $default;
}
}
?>
10 changes: 10 additions & 0 deletions app/Core/Response.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

<?php namespace App\Core;
class Response {
public static function json($data){
header('Content-Type: application/json');
echo json_encode($data);
exit;
}
}
?>
12 changes: 12 additions & 0 deletions app/Installer/EnvWriter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

<?php namespace App\Installer;
class EnvWriter {
public static function write(array $vars, $path){
$content = '';
foreach($vars as $k=>$v){
$content .= $k.'='.$v.PHP_EOL;
}
file_put_contents($path, $content);
}
}
?>
38 changes: 38 additions & 0 deletions app/Installer/Installer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@

<?php namespace App\Installer;
use App\Support\Database;
use App\Core\Env;
use PDO;

class Installer {
public static function run(){
if($_GET['token'] ?? '' !== Env::get('INSTALL_TOKEN')){
die('Invalid token');
}
if($_SERVER['REQUEST_METHOD'] === 'POST'){
$dbHost=$_POST['db_host']; $dbName=$_POST['db_name']; $dbUser=$_POST['db_user']; $dbPass=$_POST['db_pass'];
EnvWriter::write([
'APP_ENV' => 'production',
'DB_HOST' => $dbHost,
'DB_NAME' => $dbName,
'DB_USER' => $dbUser,
'DB_PASS' => $dbPass,
], __DIR__.'/../../.env');
echo 'Env written. Importing DB...<br>';
$pdo = new PDO("mysql:host={$dbHost}", $dbUser, $dbPass);
$pdo->exec("CREATE DATABASE IF NOT EXISTS `{$dbName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;");
$pdo->exec("USE `{$dbName}`;");
$pdo->exec(SqlSchema::createTable());
echo 'Done!';
return;
}
echo '<form method="post">
<input name="db_host" placeholder="DB Host" required><br>
<input name="db_name" placeholder="DB Name" required><br>
<input name="db_user" placeholder="DB User" required><br>
<input name="db_pass" placeholder="DB Pass"><br>
<button>Install</button>
</form>';
}
}
?>
35 changes: 35 additions & 0 deletions app/Installer/InstallerMock.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php
namespace App\Installer;

use App\Support\Logger;
use App\Core\Env;

class InstallerMock
{
public static function run(): void
{
if(($_GET['token'] ?? '') !== Env::get('INSTALL_TOKEN')){
die('Invalid token');
}
if($_SERVER['REQUEST_METHOD'] === 'POST'){
$dbName = $_POST['db_name'] ?? '';
$envDir = __DIR__ . '/../../storage/mock_env';
if (!is_dir($envDir)) {
mkdir($envDir, 0775, true);
}
$envContent = "APP_ENV=production\nDB_NAME={$dbName}\n";
file_put_contents($envDir . '/' . time() . '.env', $envContent);
Logger::create()->info("Mock installer ran with DB name: {$dbName}");
echo 'Env written. Importing DB...<br>';
echo 'Done!';
return;
}
echo '<form method="post">'
.' <input name="db_host" placeholder="DB Host" required><br>'
.' <input name="db_name" placeholder="DB Name" required><br>'
.' <input name="db_user" placeholder="DB User" required><br>'
.' <input name="db_pass" placeholder="DB Pass"><br>'
.' <button>Install</button>'
.' </form>';
}
}
21 changes: 21 additions & 0 deletions app/Installer/SqlSchema.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

<?php namespace App\Installer;
class SqlSchema {
public static function createTable(){
return <<<SQL
CREATE TABLE IF NOT EXISTS submissions (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(190),
email VARCHAR(190),
message TEXT,
tone VARCHAR(50),
purchase_code VARCHAR(100) UNIQUE,
product_name VARCHAR(190),
category VARCHAR(50),
ai_reply TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SQL;
}
}
?>
26 changes: 26 additions & 0 deletions app/Repository/SubmissionRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

<?php namespace App\Repository;
use PDO;

class SubmissionRepository {
protected $pdo;
public function __construct(PDO $pdo){
$this->pdo = $pdo;
}
public function save(array $data){
$stmt = $this->pdo->prepare('INSERT INTO submissions
(name,email,message,tone,purchase_code,product_name,category,ai_reply,created_at)
VALUES (?,?,?,?,?,?,?,?,NOW())');
$stmt->execute([
$data['name'],
$data['email'],
$data['message'],
$data['tone'],
$data['purchase_code'],
$data['product_name'],
$data['category'],
$data['ai_reply'],
]);
}
}
?>
14 changes: 14 additions & 0 deletions app/Repository/SubmissionRepositoryMock.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
namespace App\Repository;

class SubmissionRepositoryMock {
public function __construct($pdo = null)
{
// no-op
}

public function save(array $data): void
{
// intentionally left blank in mock mode
}
}
17 changes: 17 additions & 0 deletions app/Services/LicenseValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php
namespace App\Services;

use App\Core\Env;

class LicenseValidator {
public static function validate($code)
{
if (!$code) return [false, null];
// Simulated external API call
if ($code === 'INVALID') {
return [false, null];
}
return [true, 'Awesome Product'];
}
}
?>
Loading