openai added
This commit is contained in:
parent
5d08a44965
commit
595332008b
@ -55,6 +55,8 @@ MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
MAIL_ACTIVATION_FRONTEND=https://localhost:3156/auth/email-activation/${ACTIVATION_CODE}
|
||||
MAIL_RESET_PASS_FRONTEND=https://localhost:3156/auth/reset-password/${FORGOT_CODE}
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
@ -63,3 +65,5 @@ AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
OPENAI_MODEL=gpt-4.1-mini
|
||||
OPENAI_KEY=
|
||||
@ -7,6 +7,7 @@ HoshiAI-be — the backend part of the HoshiAI project for the Web Programming c
|
||||
* [Laravel (Sanctum for api)](https://laravel.com/)
|
||||
* [L5-Swagger](https://github.com/DarkaOnLine/L5-Swagger)
|
||||
* [mobiledetect/mobiledetectlib](https://packagist.org/packages/mobiledetect/mobiledetectlib)
|
||||
* [openai-php/client](https://github.com/openai-php/client)
|
||||
|
||||
## External API
|
||||
* [ip-api.com](http://ip-api.com/)
|
||||
@ -37,6 +38,7 @@ MAIL_PASSWORD=
|
||||
MAIL_ENCRYPTION=
|
||||
MAIL_FROM_ADDRESS=
|
||||
...
|
||||
OPENAI_MODEL=gpt-4.1-mini
|
||||
OPENAI_KEY=
|
||||
```
|
||||
4. Make migrations to your db
|
||||
|
||||
@ -26,7 +26,8 @@ class CategoryController extends Controller
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return CategoryResource::collection(Category::all());
|
||||
$categories = Category::withCount('user_tests', 'questions')->get();
|
||||
return CategoryResource::collection($categories);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -91,7 +92,7 @@ class CategoryController extends Controller
|
||||
*/
|
||||
public function show(Category $category)
|
||||
{
|
||||
return new CategoryResource($category);
|
||||
return new CategoryResource($category->withCount('user_tests', 'questions'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -7,6 +7,7 @@ use App\Models\Log;
|
||||
use App\Models\Question;
|
||||
use App\Rules\ValidVariants;
|
||||
use Illuminate\Http\Request;
|
||||
use OpenAI;
|
||||
|
||||
class QuestionController extends Controller
|
||||
{
|
||||
@ -23,8 +24,7 @@ class QuestionController extends Controller
|
||||
'variants' => ['array', 'nullable'],
|
||||
'variants.*.id' => ['required', 'integer'],
|
||||
'variants.*.text' => ['required', 'string'],
|
||||
'correct_answers' => 'required|array',
|
||||
'is_pending_question' => 'integer'
|
||||
'correct_answers' => 'required|array'
|
||||
];
|
||||
}
|
||||
/**
|
||||
@ -154,8 +154,7 @@ class QuestionController extends Controller
|
||||
* example=1
|
||||
* )
|
||||
* ),
|
||||
* @OA\Property(property="category_id", type="integer", nullable=true, example=3),
|
||||
* @OA\Property(property="is_pending_question", type="integer", example=0)
|
||||
* @OA\Property(property="category_id", type="integer", nullable=true, example=3)
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
@ -217,8 +216,7 @@ class QuestionController extends Controller
|
||||
* example=1
|
||||
* )
|
||||
* ),
|
||||
* @OA\Property(property="category_id", type="integer", nullable=true, example=2),
|
||||
* @OA\Property(property="is_pending_question", type="integer", example=0)
|
||||
* @OA\Property(property="category_id", type="integer", nullable=true, example=2)
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
@ -272,4 +270,163 @@ class QuestionController extends Controller
|
||||
|
||||
return ['message' => 'The question was deleted'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/questions/openai-generate",
|
||||
* summary="Generate a question using OpenAI (Creator or Admin)",
|
||||
* description="Generates a new question via OpenAI API. Only authorized users (creator or admin) can call this endpoint.",
|
||||
* tags={"Questions"},
|
||||
* security={{"bearerAuth":{}}},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"type","category_id","language","difficulty","promt"},
|
||||
* @OA\Property(property="type", type="string", description="Type of the question", enum={"single","multiple","text"}, example="single"),
|
||||
* @OA\Property(property="category_id", type="integer", description="ID of the category", example=3),
|
||||
* @OA\Property(property="language", type="string", description="Language for AI prompt", example="en"),
|
||||
* @OA\Property(property="difficulty", type="integer", description="Difficulty level (0-10)", example=5),
|
||||
* @OA\Property(property="promt", type="string", description="The prompt for AI to generate the question", example="Explain HTTP methods")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="AI-generated question",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="question", type="object",
|
||||
* @OA\Property(property="title", type="string", example="Sample question"),
|
||||
* @OA\Property(property="description", type="string", example="Optional description"),
|
||||
* @OA\Property(property="type", type="string", enum={"single","multiple","text"}),
|
||||
* @OA\Property(property="difficulty", type="integer", example=5),
|
||||
* @OA\Property(property="variants", type="array",
|
||||
* @OA\Items(
|
||||
* @OA\Property(property="id", type="integer"),
|
||||
* @OA\Property(property="text", type="string")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Property(property="correct_answers", type="array",
|
||||
* @OA\Items(oneOf={@OA\Schema(type="integer"), @OA\Schema(type="string")})
|
||||
* ),
|
||||
* @OA\Property(property="category_id", type="integer", example=3)
|
||||
* )
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=422,
|
||||
* description="Validation error",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="message", type="string"),
|
||||
* @OA\Property(property="errors", type="object")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=500,
|
||||
* description="AI returned invalid JSON",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="error", type="string"),
|
||||
* @OA\Property(property="raw", type="string")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function openai_generate(Request $request)
|
||||
{
|
||||
$this->authorize('create', Question::class);
|
||||
|
||||
$fields = $request->validate([
|
||||
'type' => 'required|string|in:single,multiple,text',
|
||||
'category_id' => 'required|exists:categories,id',
|
||||
'language' => 'required|string',
|
||||
'difficulty' => 'required|integer',
|
||||
'promt' => 'required|string'
|
||||
]);
|
||||
$type = $fields['type'];
|
||||
$lang = $fields['language'];
|
||||
$difficulty = $fields['difficulty'];
|
||||
$cat_id = $fields['category_id'];
|
||||
$promt_json = json_encode($fields['promt']);
|
||||
|
||||
$question_type_promt = "";
|
||||
$answer = "";
|
||||
$variations = <<< EOT
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"text": "Option 1"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"text": "Option 2"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"text": "Option 3"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"text": "Option 4"
|
||||
}
|
||||
]
|
||||
EOT;
|
||||
switch ($type) {
|
||||
case 'single':
|
||||
$question_type_promt = "Create 4 answer options, with one of them being correct (the \"correct_answers\" field is an array containing the ID of the correct option).";
|
||||
$answer = json_encode([2]); ;
|
||||
break;
|
||||
case 'multiple':
|
||||
$question_type_promt = "Create between 4 and 6 answer options, with several of them being correct (the \"correct_answers\" field is an array containing the IDs of the correct options).";
|
||||
$answer = json_encode([1,2]); ;
|
||||
break;
|
||||
case 'text':
|
||||
$question_type_promt = "Generate a question where there can only be a single, precise answer that can be written without an extended explanation. The \"correct_answers\" field should be an array containing only ONE answer as a string. The \"variations\" must be empty array";
|
||||
$answer = json_encode(["Answer"]);
|
||||
$variations = '[]';
|
||||
break;
|
||||
}
|
||||
|
||||
$openai_promt = <<< EOT
|
||||
Generate 1 question using the following template (this is example):
|
||||
|
||||
{
|
||||
"title": "Sample question",
|
||||
"description": "Optional description",
|
||||
"type": "$type",
|
||||
"difficulty": $difficulty,
|
||||
"variants": $variations,
|
||||
"correct_answers": $answer,
|
||||
"category_id": $cat_id
|
||||
}
|
||||
|
||||
You can change only variants, correct_answers, description and title.
|
||||
Your response should be only JSON with no explanations, exactly following the template.
|
||||
|
||||
$question_type_promt
|
||||
|
||||
Question difficulty: $difficulty/10
|
||||
The question should be based on the following prompt with language $lang:
|
||||
$promt_json
|
||||
|
||||
EOT;
|
||||
$client = OpenAI::client(env("OPENAI_KEY"));
|
||||
|
||||
$response = $client->responses()->create([
|
||||
'model' => env('OPENAI_MODEL', 'gpt-4.1-mini'),
|
||||
'input' => $openai_promt,
|
||||
'temperature' => 0,
|
||||
]);
|
||||
$ai_answer = $response->outputText;
|
||||
|
||||
$question_json = json_decode($ai_answer, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
return response()->json([
|
||||
'error' => 'AI returned invalid JSON',
|
||||
'raw' => $ai_answer,
|
||||
], 500);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'question' => $question_json,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ class UserTestController extends Controller
|
||||
{
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/user-tests",
|
||||
* path="/api/user-tests/me",
|
||||
* summary="Get list of user tests for the authenticated user",
|
||||
* description="Retrieve all UserTests belonging to the authenticated user, with related category, test, and answers.",
|
||||
* tags={"UserTests"},
|
||||
@ -34,7 +34,7 @@ class UserTestController extends Controller
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function index(Request $request)
|
||||
public function me(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
$userTests = UserTest::where('user_id', $user->id)
|
||||
@ -45,25 +45,97 @@ class UserTestController extends Controller
|
||||
return UserTestResource::collection($userTests);
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/user-tests",
|
||||
* summary="Get a list of user tests (only creator or admin)",
|
||||
* description="Returns all user tests. Only the creator or an admin can access this endpoint.",
|
||||
* tags={"UserTests"},
|
||||
* security={{"sanctum":{}}},
|
||||
* @OA\Parameter(
|
||||
* name="test_id",
|
||||
* in="query",
|
||||
* description="Filter by Test ID",
|
||||
* required=false,
|
||||
* @OA\Schema(type="integer", example=5)
|
||||
* ),
|
||||
* @OA\Parameter(
|
||||
* name="question_id",
|
||||
* in="query",
|
||||
* description="Filter by Question ID (via answers.question)",
|
||||
* required=false,
|
||||
* @OA\Schema(type="integer", example=12)
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="List of user tests retrieved successfully",
|
||||
* @OA\JsonContent(
|
||||
* type="array",
|
||||
* @OA\Items(ref="#/components/schemas/UserTestResource")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthenticated — missing or invalid token"
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=403,
|
||||
* description="Forbidden — only the creator or admin can access this resource"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->authorize('viewAny', UserTest::class);
|
||||
|
||||
$query = UserTest::query()
|
||||
->with(['user', 'category', 'answers.question'])
|
||||
->orderBy('id', 'desc');
|
||||
|
||||
// filter by test_id
|
||||
if ($request->has('test_id')) {
|
||||
$query->where('test_id', $request->input('test_id'));
|
||||
}
|
||||
|
||||
// filter by question_id
|
||||
if ($request->has('question_id')) {
|
||||
$questionId = $request->input('question_id');
|
||||
$query->whereHas('answers.question', function ($q) use ($questionId) {
|
||||
$q->where('questions.id', $questionId);
|
||||
});
|
||||
}
|
||||
|
||||
$userTests = $query->get();
|
||||
|
||||
return UserTestResource::collection($userTests);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/user-tests",
|
||||
* summary="Create a new user test",
|
||||
* description="Generate a new UserTest for the authenticated user with questions from a specific category and difficulty range.",
|
||||
* tags={"UserTests"},
|
||||
* security={{"bearerAuth":{}}},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(
|
||||
* required={"category_id"},
|
||||
* @OA\Property(property="category_id", type="integer", example=3, description="Category from which to select questions"),
|
||||
* @OA\Property(property="min_difficulty", type="integer", nullable=true, example=0, description="Minimum difficulty of questions"),
|
||||
* @OA\Property(property="max_difficulty", type="integer", nullable=true, example=10, description="Maximum difficulty of questions")
|
||||
* @OA\Property(property="category_id", type="integer", example=3),
|
||||
* @OA\Property(property="min_difficulty", type="integer", example=0),
|
||||
* @OA\Property(property="max_difficulty", type="integer", example=10)
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="User test created successfully",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="message", type="string", example="This test isn't available now!")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=422,
|
||||
* description="Validation error or no questions found"
|
||||
* description="Validation error"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
|
||||
@ -28,10 +28,16 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
||||
* example="2025-11-10T18:00:00Z"
|
||||
* ),
|
||||
* @OA\Property(
|
||||
* property="count",
|
||||
* property="questions_count",
|
||||
* type="integer",
|
||||
* example=5,
|
||||
* description="Dynamic count of related items"
|
||||
* description="Dynamic count of questions by category"
|
||||
* ),
|
||||
* @OA\Property(
|
||||
* property="user_tests_count",
|
||||
* type="integer",
|
||||
* example=5,
|
||||
* description="Dynamic count of user tests"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
@ -48,7 +54,8 @@ class CategoryResource extends JsonResource
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'created_at' => $this->created_at,
|
||||
'count' => $this->count ?? 0
|
||||
'questions_count' => $this->when(isset($this->questions_count), $this->questions_count, 0),
|
||||
'user_tests_count' => $this->when(isset($this->user_tests_count), $this->user_tests_count, 0),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,4 +13,16 @@ class Category extends Model
|
||||
protected $fillable = [
|
||||
'name'
|
||||
];
|
||||
public function tests()
|
||||
{
|
||||
return $this->hasMany(Test::class, 'category_id');
|
||||
}
|
||||
public function user_tests()
|
||||
{
|
||||
return $this->hasMany(UserTest::class, 'category_id');
|
||||
}
|
||||
public function questions()
|
||||
{
|
||||
return $this->hasMany(Question::class, 'category_id');
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,4 +43,6 @@ class Test extends Model
|
||||
return $this->belongsToMany(Question::class)
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -13,7 +13,7 @@ class UserTestPolicy
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return true;
|
||||
return $user->type == 'admin' || $user->type == 'creator';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -12,7 +12,8 @@
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"mobiledetect/mobiledetectlib": "^4.8"
|
||||
"mobiledetect/mobiledetectlib": "^4.8",
|
||||
"openai-php/client": "^0.18.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
|
||||
228
composer.lock
generated
228
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "a58d7ff4f8de3898bf7fa6350013d42a",
|
||||
"content-hash": "dec861e2bbb0078cfccbd798a66b1404",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
@ -2950,6 +2950,232 @@
|
||||
],
|
||||
"time": "2025-10-18T11:10:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "openai-php/client",
|
||||
"version": "v0.18.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/openai-php/client.git",
|
||||
"reference": "3362ab004fcfc9d77df3aff7671fbcbe70177cae"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/openai-php/client/zipball/3362ab004fcfc9d77df3aff7671fbcbe70177cae",
|
||||
"reference": "3362ab004fcfc9d77df3aff7671fbcbe70177cae",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.2.0",
|
||||
"php-http/discovery": "^1.20.0",
|
||||
"php-http/multipart-stream-builder": "^1.4.2",
|
||||
"psr/http-client": "^1.0.3",
|
||||
"psr/http-client-implementation": "^1.0.1",
|
||||
"psr/http-factory-implementation": "*",
|
||||
"psr/http-message": "^1.1.0|^2.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"guzzlehttp/guzzle": "^7.9.3",
|
||||
"guzzlehttp/psr7": "^2.7.1",
|
||||
"laravel/pint": "^1.24.0",
|
||||
"mockery/mockery": "^1.6.12",
|
||||
"nunomaduro/collision": "^8.8.0",
|
||||
"pestphp/pest": "^3.8.2|^4.0.0",
|
||||
"pestphp/pest-plugin-arch": "^3.1.1|^4.0.0",
|
||||
"pestphp/pest-plugin-type-coverage": "^3.5.1|^4.0.0",
|
||||
"phpstan/phpstan": "^1.12.25",
|
||||
"symfony/var-dumper": "^7.2.6"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/OpenAI.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"OpenAI\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nuno Maduro",
|
||||
"email": "enunomaduro@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Sandro Gehri"
|
||||
}
|
||||
],
|
||||
"description": "OpenAI PHP is a supercharged PHP API client that allows you to interact with the Open AI API",
|
||||
"keywords": [
|
||||
"GPT-3",
|
||||
"api",
|
||||
"client",
|
||||
"codex",
|
||||
"dall-e",
|
||||
"language",
|
||||
"natural",
|
||||
"openai",
|
||||
"php",
|
||||
"processing",
|
||||
"sdk"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/openai-php/client/issues",
|
||||
"source": "https://github.com/openai-php/client/tree/v0.18.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://www.paypal.com/paypalme/enunomaduro",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/gehrisandro",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nunomaduro",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-10-31T18:58:57+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-http/discovery",
|
||||
"version": "1.20.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-http/discovery.git",
|
||||
"reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d",
|
||||
"reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"composer-plugin-api": "^1.0|^2.0",
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"nyholm/psr7": "<1.0",
|
||||
"zendframework/zend-diactoros": "*"
|
||||
},
|
||||
"provide": {
|
||||
"php-http/async-client-implementation": "*",
|
||||
"php-http/client-implementation": "*",
|
||||
"psr/http-client-implementation": "*",
|
||||
"psr/http-factory-implementation": "*",
|
||||
"psr/http-message-implementation": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"composer/composer": "^1.0.2|^2.0",
|
||||
"graham-campbell/phpspec-skip-example-extension": "^5.0",
|
||||
"php-http/httplug": "^1.0 || ^2.0",
|
||||
"php-http/message-factory": "^1.0",
|
||||
"phpspec/phpspec": "^5.1 || ^6.1 || ^7.3",
|
||||
"sebastian/comparator": "^3.0.5 || ^4.0.8",
|
||||
"symfony/phpunit-bridge": "^6.4.4 || ^7.0.1"
|
||||
},
|
||||
"type": "composer-plugin",
|
||||
"extra": {
|
||||
"class": "Http\\Discovery\\Composer\\Plugin",
|
||||
"plugin-optional": true
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Http\\Discovery\\": "src/"
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"src/Composer/Plugin.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Márk Sági-Kazár",
|
||||
"email": "mark.sagikazar@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations",
|
||||
"homepage": "http://php-http.org",
|
||||
"keywords": [
|
||||
"adapter",
|
||||
"client",
|
||||
"discovery",
|
||||
"factory",
|
||||
"http",
|
||||
"message",
|
||||
"psr17",
|
||||
"psr7"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-http/discovery/issues",
|
||||
"source": "https://github.com/php-http/discovery/tree/1.20.0"
|
||||
},
|
||||
"time": "2024-10-02T11:20:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-http/multipart-stream-builder",
|
||||
"version": "1.4.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-http/multipart-stream-builder.git",
|
||||
"reference": "10086e6de6f53489cca5ecc45b6f468604d3460e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-http/multipart-stream-builder/zipball/10086e6de6f53489cca5ecc45b6f468604d3460e",
|
||||
"reference": "10086e6de6f53489cca5ecc45b6f468604d3460e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0",
|
||||
"php-http/discovery": "^1.15",
|
||||
"psr/http-factory-implementation": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"nyholm/psr7": "^1.0",
|
||||
"php-http/message": "^1.5",
|
||||
"php-http/message-factory": "^1.0.2",
|
||||
"phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.3"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Http\\Message\\MultipartStream\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Tobias Nyholm",
|
||||
"email": "tobias.nyholm@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "A builder class that help you create a multipart stream",
|
||||
"homepage": "http://php-http.org",
|
||||
"keywords": [
|
||||
"factory",
|
||||
"http",
|
||||
"message",
|
||||
"multipart stream",
|
||||
"stream"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-http/multipart-stream-builder/issues",
|
||||
"source": "https://github.com/php-http/multipart-stream-builder/tree/1.4.2"
|
||||
},
|
||||
"time": "2024-09-04T13:22:54+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoption/phpoption",
|
||||
"version": "1.9.4",
|
||||
|
||||
@ -14,6 +14,7 @@ use Illuminate\Support\Facades\Route;
|
||||
// UserTests
|
||||
Route::middleware('auth:sanctum')->group(function () {
|
||||
Route::get('user-tests', [UserTestController::class, 'index']);
|
||||
Route::get('user-tests/me', [UserTestController::class, 'me']);
|
||||
Route::post('user-tests', [UserTestController::class, 'store']);
|
||||
Route::post('user-tests/by-test', [UserTestController::class, 'storeByTest']);
|
||||
|
||||
@ -42,6 +43,7 @@ Route::get('questions', [QuestionController::class, 'index']);
|
||||
Route::get('questions/{question}', [QuestionController::class, 'show']);
|
||||
|
||||
Route::middleware('auth:sanctum')->group(function () {
|
||||
Route::post('questions/openai-generate', [QuestionController::class, 'openai_generate']);
|
||||
Route::post('questions', [QuestionController::class, 'store']);
|
||||
Route::put('questions/{question}', [QuestionController::class, 'update']);
|
||||
Route::delete('questions/{question}', [QuestionController::class, 'destroy']);
|
||||
|
||||
@ -1246,10 +1246,6 @@
|
||||
"type": "integer",
|
||||
"example": 3,
|
||||
"nullable": true
|
||||
},
|
||||
"is_pending_question": {
|
||||
"type": "integer",
|
||||
"example": 0
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@ -1389,10 +1385,6 @@
|
||||
"type": "integer",
|
||||
"example": 2,
|
||||
"nullable": true
|
||||
},
|
||||
"is_pending_question": {
|
||||
"type": "integer",
|
||||
"example": 0
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@ -1461,6 +1453,176 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/questions/openai-generate": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Questions"
|
||||
],
|
||||
"summary": "Generate a question using OpenAI (Creator or Admin)",
|
||||
"description": "Generates a new question via OpenAI API. Only authorized users (creator or admin) can call this endpoint.",
|
||||
"operationId": "89ba54de2578b1b1f2e75d559ef4ef6d",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"required": [
|
||||
"type",
|
||||
"category_id",
|
||||
"language",
|
||||
"difficulty",
|
||||
"promt"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"description": "Type of the question",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"single",
|
||||
"multiple",
|
||||
"text"
|
||||
],
|
||||
"example": "single"
|
||||
},
|
||||
"category_id": {
|
||||
"description": "ID of the category",
|
||||
"type": "integer",
|
||||
"example": 3
|
||||
},
|
||||
"language": {
|
||||
"description": "Language for AI prompt",
|
||||
"type": "string",
|
||||
"example": "en"
|
||||
},
|
||||
"difficulty": {
|
||||
"description": "Difficulty level (0-10)",
|
||||
"type": "integer",
|
||||
"example": 5
|
||||
},
|
||||
"promt": {
|
||||
"description": "The prompt for AI to generate the question",
|
||||
"type": "string",
|
||||
"example": "Explain HTTP methods"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "AI-generated question",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"question": {
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Sample question"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "Optional description"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"single",
|
||||
"multiple",
|
||||
"text"
|
||||
]
|
||||
},
|
||||
"difficulty": {
|
||||
"type": "integer",
|
||||
"example": 5
|
||||
},
|
||||
"variants": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"correct_answers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"category_id": {
|
||||
"type": "integer",
|
||||
"example": 3
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"errors": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "AI returned invalid JSON",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"raw": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/tests": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@ -2153,14 +2315,14 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/user-tests": {
|
||||
"/api/user-tests/me": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"UserTests"
|
||||
],
|
||||
"summary": "Get list of user tests for the authenticated user",
|
||||
"description": "Retrieve all UserTests belonging to the authenticated user, with related category, test, and answers.",
|
||||
"operationId": "bf312e7c74799e8dbaa29025b8f52f46",
|
||||
"operationId": "fef73e47f9c3188bea00ab44f7169105",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of user tests",
|
||||
@ -2186,13 +2348,70 @@
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/user-tests": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"UserTests"
|
||||
],
|
||||
"summary": "Get a list of user tests (only creator or admin)",
|
||||
"description": "Returns all user tests. Only the creator or an admin can access this endpoint.",
|
||||
"operationId": "bf312e7c74799e8dbaa29025b8f52f46",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "test_id",
|
||||
"in": "query",
|
||||
"description": "Filter by Test ID",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"example": 5
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "question_id",
|
||||
"in": "query",
|
||||
"description": "Filter by Question ID (via answers.question)",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"example": 12
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of user tests retrieved successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/UserTestResource"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthenticated — missing or invalid token"
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden — only the creator or admin can access this resource"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"sanctum": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"UserTests"
|
||||
],
|
||||
"summary": "Create a new user test",
|
||||
"description": "Generate a new UserTest for the authenticated user with questions from a specific category and difficulty range.",
|
||||
"operationId": "dab4526b31f8a1bf1abf3048c3a4ecbe",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@ -2204,21 +2423,16 @@
|
||||
],
|
||||
"properties": {
|
||||
"category_id": {
|
||||
"description": "Category from which to select questions",
|
||||
"type": "integer",
|
||||
"example": 3
|
||||
},
|
||||
"min_difficulty": {
|
||||
"description": "Minimum difficulty of questions",
|
||||
"type": "integer",
|
||||
"example": 0,
|
||||
"nullable": true
|
||||
"example": 0
|
||||
},
|
||||
"max_difficulty": {
|
||||
"description": "Maximum difficulty of questions",
|
||||
"type": "integer",
|
||||
"example": 10,
|
||||
"nullable": true
|
||||
"example": 10
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@ -2227,8 +2441,24 @@
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "User test created successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "This test isn't available now!"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation error or no questions found"
|
||||
"description": "Validation error"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
@ -2582,8 +2812,13 @@
|
||||
"format": "date-time",
|
||||
"example": "2025-11-10T18:00:00Z"
|
||||
},
|
||||
"count": {
|
||||
"description": "Dynamic count of related items",
|
||||
"questions_count": {
|
||||
"description": "Dynamic count of questions by category",
|
||||
"type": "integer",
|
||||
"example": 5
|
||||
},
|
||||
"user_tests_count": {
|
||||
"description": "Dynamic count of user tests",
|
||||
"type": "integer",
|
||||
"example": 5
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user