feat(06-task-title-ai): complete OpenAI task title suggestions
Phase 6 complete: - add AI title generator service using gpt-5-nano - add task popup button for biuro@project-pro.pl - add AJAX endpoint returning title suggestions without auto-save
This commit is contained in:
@@ -11,7 +11,7 @@ Użytkownicy mogą efektywnie zarządzać projektami, zadaniami i klientami w je
|
||||
| Metric | Value | Source |
|
||||
|--------|-------|--------|
|
||||
| Version | v0.1.0 (in progress) | Milestone |
|
||||
| Status | Phase 1 and Phase 5 complete, Phase 2 next | STATE.md |
|
||||
| Status | Phase 1, Phase 5 and Phase 6 complete, Phase 2 next | STATE.md |
|
||||
| Lines of Code | 9 356 | SonarQube baseline |
|
||||
| Bugs | 58 | SonarQube baseline |
|
||||
| Code Smells | 1 649 | SonarQube baseline |
|
||||
@@ -25,6 +25,7 @@ Użytkownicy mogą efektywnie zarządzać projektami, zadaniami i klientami w je
|
||||
### Validated
|
||||
- ✓ SonarQube baseline — Phase 1: projekt skonfigurowany, skan wykonany, metryki udokumentowane
|
||||
- ✓ Import finansow z Fakturowni — Phase 5: automatyczny import faktur przychodowych/kosztowych, mapowanie po NIP, filtr proforma, skip-list pozycji, edycja dopasowania kategorii
|
||||
- ✓ AI title generation for tasks — Phase 6: przycisk AI w popupie zadania dla `biuro@project-pro.pl`, propozycja tytulu bez automatycznego zapisu
|
||||
|
||||
### Active
|
||||
- [ ] Naprawić 58 bugów (priorytet: 3 CRITICAL, 35 MAJOR)
|
||||
@@ -49,6 +50,8 @@ Użytkownicy mogą efektywnie zarządzać projektami, zadaniami i klientami w je
|
||||
| Filtr proforma: kind + prefiks FP | Phase 5 | Proformy nie trafiaja do finansow |
|
||||
| Edycja dopasowania Fakturownia tylko dla operacji importowanych | Phase 5 | Reczne operacje pozostaja przy standardowej edycji kategorii |
|
||||
| Zmiana dopasowania moze masowo przepiac pasujace operacje | Phase 5 | Szybka korekta blednej kategorii dla powtarzalnych pozycji faktur |
|
||||
| Tytuly AI sa propozycja, nie automatycznym zapisem | Phase 6 | Bezpieczna edycja tytulu zadania przez istniejacy mechanizm zapisu |
|
||||
| Model tytulow zadan: `gpt-5-nano` | Phase 6 | Osobna konfiguracja od importu maili i niski koszt generowania |
|
||||
|
||||
## Success Criteria
|
||||
- Użytkownicy mogą efektywnie zarządzać projektami, zadaniami i klientami w jednym systemie CRM
|
||||
@@ -58,5 +61,6 @@ Użytkownicy mogą efektywnie zarządzać projektami, zadaniami i klientami w je
|
||||
|
||||
---
|
||||
*Created: 2026-03-15*
|
||||
*Last updated: 2026-05-04 after Phase 5*
|
||||
*Last updated: 2026-05-04 after Phase 6*
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ Stabilizacja i poprawa jakosci kodu crmPRO oraz rozwoj finansow o automatyczny i
|
||||
## Current Milestone
|
||||
**v0.1 Stabilizacja i jakosc kodu + import finansow** (v0.1.0)
|
||||
Status: In progress
|
||||
Phases: 2 of 5 complete
|
||||
Phases: 3 of 6 complete
|
||||
|
||||
## Phases
|
||||
|
||||
@@ -17,6 +17,7 @@ Phases: 2 of 5 complete
|
||||
| 3 | Naprawa bledow glownych | TBD | Not started | - |
|
||||
| 4 | Poprawa pokrycia testami | TBD | Not started | - |
|
||||
| 5 | Import finansow z Fakturowni | 6/6 | Complete | 2026-05-04 |
|
||||
| 6 | AI title generation for tasks | 1/1 | Complete | 2026-05-04 |
|
||||
|
||||
## Phase Details
|
||||
|
||||
@@ -96,9 +97,24 @@ Phases: 2 of 5 complete
|
||||
- [x] 05-05: Skip-list pozycji — mozliwosc oznaczenia wybranej pozycji faktury jako pomijanej (nie trafia do finance_operations)
|
||||
- [x] 05-06: Edycja dopasowania kategorii z poziomu operation_edit + opcjonalne masowe przepiecie operacji z tym samym itemem Fakturownia
|
||||
|
||||
### Phase 6: AI title generation for tasks
|
||||
|
||||
**Goal:** Dodac w popupie zadania przycisk generowania propozycji tytulu przez OpenAI na podstawie tresci zadania, widoczny tylko dla `biuro@project-pro.pl`
|
||||
**Depends on:** None (niezalezna funkcja w module zadan)
|
||||
**Research:** Done (OpenAI official model docs)
|
||||
|
||||
**Scope:**
|
||||
- Przycisk AI w `templates/tasks/task_popup.php` tylko dla zalogowanego `biuro@project-pro.pl`
|
||||
- Backend AJAX generujacy propozycje tytulu bez zapisu do bazy
|
||||
- Osobny serwis domenowy OpenAI z modelem `gpt-5-nano`
|
||||
|
||||
**Plans:**
|
||||
- [x] 06-01: Generowanie propozycji tytulu zadania przez OpenAI w popupie
|
||||
---
|
||||
*Roadmap created: 2026-03-15*
|
||||
*Last updated: 2026-05-04 - Phase 5 complete (05-01..05-06)*
|
||||
*Last updated: 2026-05-04 - Phase 6 complete (06-01)*
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -9,14 +9,14 @@ See: .paul/PROJECT.md (updated 2026-05-04)
|
||||
|
||||
## Current Position
|
||||
|
||||
Milestone: v0.1 Stabilizacja i jakosc kodu
|
||||
Phase: 2 of 5 (Naprawa bledow krytycznych) - Ready to plan
|
||||
Milestone: v0.1 Stabilizacja i jakosc kodu + import finansow
|
||||
Phase: 2 of 6 (Naprawa bledow krytycznych) - Ready to plan
|
||||
Plan: Not started
|
||||
Status: Phase 5 complete, ready for next PLAN
|
||||
Last activity: 2026-05-04 22:54 - Phase 5 complete; 05-06 UNIFY closed
|
||||
Status: Phase 6 complete, ready for next PLAN
|
||||
Last activity: 2026-05-04 23:45 - Phase 6 complete; 06-01 UNIFY closed
|
||||
|
||||
Progress:
|
||||
- Milestone: [####------] 40% (2 of 5 phases complete: Phase 1 and Phase 5)
|
||||
- Milestone: [#####-----] 50% (3 of 6 phases complete: Phase 1, Phase 5 and Phase 6)
|
||||
- Phase 2: [----------] 0%
|
||||
|
||||
## Loop Position
|
||||
@@ -30,6 +30,11 @@ PLAN --> APPLY --> UNIFY
|
||||
## Accumulated Context
|
||||
|
||||
### Recent Decisions
|
||||
- Phase 6: AI title generation is visible only for logged-in `biuro@project-pro.pl`.
|
||||
- Phase 6: Generated title is inserted into the inline title input only; existing save action persists it.
|
||||
- Phase 6: Use `gpt-5-nano` as the cheap/fast OpenAI model for task title suggestions.
|
||||
- Phase 6: GPT-5 title generation uses `reasoning_effort = minimal` and higher `max_completion_tokens` to avoid empty content.
|
||||
- Phase 6: Prompt requires short bezosobowy noun-style titles, e.g. `Usuniecie bloku o firmie`.
|
||||
- Phase 5: Fakturownia client mappings use tax-based keys with legacy id fallback.
|
||||
- Phase 5: Proforma documents are skipped by kind and FP prefix.
|
||||
- Phase 5: Selected invoice positions can be skipped without creating finance operations.
|
||||
@@ -38,16 +43,17 @@ PLAN --> APPLY --> UNIFY
|
||||
|
||||
### Concerns
|
||||
- Existing Fakturownia operations identify item mapping from operation description plus mapping fallback. Future importer work should store explicit operation_id -> item_key metadata.
|
||||
- OpenAI API key remains configured directly in `config.php`; this was pre-existing and should be considered for future secret management cleanup.
|
||||
|
||||
### Git State
|
||||
Last commit: 7acf22c
|
||||
Last commit: pending phase transition commit
|
||||
Branch: main
|
||||
Feature branches merged: none
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-05-04
|
||||
Stopped at: Phase 5 complete, ready to plan Phase 2
|
||||
Stopped at: Phase 6 complete, ready to plan Phase 2
|
||||
Next action: $paul-plan for Phase 2
|
||||
Resume file: .paul/ROADMAP.md
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
- Naprawiono zapis formularza przez usuniecie zagniezdzonego formularza w `gridEdit`.
|
||||
- Zamknieto formalnie Phase 5 jako kompletna.
|
||||
|
||||
- [Phase 6, Plan 06-01] Dodano generowanie propozycji tytulu zadania przez OpenAI w popupie zadania dla `biuro@project-pro.pl`.
|
||||
- Dodano endpoint AJAX, ktory zwraca propozycje tytulu bez zapisu do bazy.
|
||||
- Dodano serwis `TaskTitleGenerator` z modelem `gpt-5-nano` i promptem na krotki tytul bezosobowy.
|
||||
|
||||
## Zmienione pliki
|
||||
|
||||
- `.paul/PROJECT.md`
|
||||
@@ -20,3 +24,10 @@
|
||||
- `autoload/Controllers/FinancesController.php`
|
||||
- `autoload/Domain/Finances/FinanceRepository.php`
|
||||
- `templates/finances/operation-edit.php`
|
||||
- `.paul/phases/06-task-title-ai/06-01-PLAN.md`
|
||||
- `.paul/phases/06-task-title-ai/06-01-SUMMARY.md`
|
||||
- `autoload/Domain/Tasks/TaskTitleGenerator.php`
|
||||
- `autoload/controls/class.Tasks.php`
|
||||
- `config.php`
|
||||
- `templates/tasks/task_popup.php`
|
||||
|
||||
|
||||
201
.paul/phases/06-task-title-ai/06-01-PLAN.md
Normal file
201
.paul/phases/06-task-title-ai/06-01-PLAN.md
Normal file
@@ -0,0 +1,201 @@
|
||||
---
|
||||
phase: 06-task-title-ai
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- config.php
|
||||
- autoload/Domain/Tasks/TaskTitleGenerator.php
|
||||
- autoload/controls/class.Tasks.php
|
||||
- templates/tasks/task_popup.php
|
||||
autonomous: false
|
||||
delegation: off
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Dodac w popupie edycji zadania przycisk generowania propozycji tytulu zadania przez OpenAI na podstawie tresci zadania.
|
||||
|
||||
## Purpose
|
||||
Uzytkownik `biuro@project-pro.pl` ma szybciej nadawac czytelne tytuly zadaniom bez recznego streszczania opisu.
|
||||
|
||||
## Output
|
||||
- Przycisk AI obok istniejacego przycisku edycji tytulu w `task_popup`.
|
||||
- Osobny endpoint AJAX zwracajacy wygenerowana propozycje tytulu.
|
||||
- Serwis domenowy do komunikacji z OpenAI, uzywajacy taniego i szybkiego modelu `gpt-5-nano`.
|
||||
- Konfiguracja modelu tytulow niezalezna od importu maili.
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
<clarifications>
|
||||
- **[Widocznosc]** - Czy `biuro@project-pro.pl` oznacza zalogowanego uzytkownika czy zadania przypisane do tego uzytkownika?
|
||||
-> Odpowiedz: tylko zalogowany uzytkownik z tym emailem widzi przycisk.
|
||||
- **[Zapis]** - Czy AI ma od razu nadpisac tytul w bazie czy tylko wstawic propozycje do pola?
|
||||
-> Odpowiedz: opcja A, wstawic propozycje do pola tytulu i zapisac dopiero istniejacym przyciskiem.
|
||||
- **[Model]** - Czy uzyc `gpt-5-nano` jako tani/szybki model?
|
||||
-> Odpowiedz: tak.
|
||||
</clarifications>
|
||||
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
@.paul/STATE.md
|
||||
|
||||
## Source Files
|
||||
@config.php
|
||||
@autoload/Domain/Tasks/MailToTaskImporter.php
|
||||
@autoload/controls/class.Tasks.php
|
||||
@templates/tasks/task_popup.php
|
||||
@templates/tasks/main_view.php
|
||||
|
||||
## External References
|
||||
- OpenAI model docs: https://developers.openai.com/api/docs/models/gpt-5-nano
|
||||
- `gpt-5-nano` supports `v1/chat/completions` and is documented as the fastest/cheapest GPT-5 option for summarization/classification-style work.
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Przycisk widoczny tylko dla biuro
|
||||
```gherkin
|
||||
Given zalogowany uzytkownik ma email biuro@project-pro.pl
|
||||
When otwiera popup zadania
|
||||
Then obok przycisku edycji tytulu widzi przycisk generowania tytulu AI
|
||||
```
|
||||
|
||||
## AC-2: Brak przycisku dla innych uzytkownikow
|
||||
```gherkin
|
||||
Given zalogowany uzytkownik ma inny email niz biuro@project-pro.pl
|
||||
When otwiera popup zadania
|
||||
Then przycisk generowania tytulu AI nie jest renderowany
|
||||
And endpoint generowania odrzuca zadanie bez wywolania OpenAI
|
||||
```
|
||||
|
||||
## AC-3: AI generuje propozycje bez zapisu do bazy
|
||||
```gherkin
|
||||
Given uzytkownik biuro@project-pro.pl otwiera popup zadania z trescia
|
||||
When klika przycisk generowania tytulu AI
|
||||
Then system pobiera tresc zadania i zwraca krotka propozycje tytulu
|
||||
And propozycja trafia do pola edycji tytulu
|
||||
And tytul w bazie nie zmienia sie przed kliknieciem istniejacego przycisku zapisu
|
||||
```
|
||||
|
||||
## AC-4: Obsluga bledow OpenAI jest czytelna
|
||||
```gherkin
|
||||
Given brakuje klucza API OpenAI albo OpenAI zwroci blad
|
||||
When uzytkownik klika generowanie tytulu
|
||||
Then popup pokazuje czytelny komunikat bledu
|
||||
And aktualny tytul zadania nie zostaje zmieniony
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Dodac serwis generowania tytulu przez OpenAI</name>
|
||||
<files>config.php, autoload/Domain/Tasks/TaskTitleGenerator.php</files>
|
||||
<action>
|
||||
Dodac konfiguracje `openai_task_title_model` z domyslna wartoscia `gpt-5-nano`.
|
||||
Utworzyc klase domenowa `Domain\Tasks\TaskTitleGenerator`, ktora:
|
||||
- przyjmuje klucz API i model z konfiguracji,
|
||||
- usuwa HTML z tresci zadania przed wyslaniem,
|
||||
- wysyla krotki prompt do `https://api.openai.com/v1/chat/completions`,
|
||||
- wymusza pojedynczy, krotki tytul bez JSON i bez dodatkowego komentarza,
|
||||
- ogranicza dlugosc wejscia i wyjscia,
|
||||
- zwraca tablice sukces/blad bez rzucania nieobslugiwanych wyjatkow.
|
||||
Reuzyc wzorce z `MailToTaskImporter`, ale nie uzalezniac nowej funkcji od importera maili.
|
||||
Dla modeli `gpt-5*` uzyc parametru `max_completion_tokens`; dla pozostalych zostawic kompatybilny fallback.
|
||||
</action>
|
||||
<verify>C:\xampp\php\php.exe -l autoload/Domain/Tasks/TaskTitleGenerator.php</verify>
|
||||
<done>AC-3 i AC-4 maja pokrycie po stronie integracji OpenAI</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Dodac endpoint AJAX dla propozycji tytulu</name>
|
||||
<files>autoload/controls/class.Tasks.php</files>
|
||||
<action>
|
||||
Dodac metode kontrolera np. `task_generate_title`, ktora:
|
||||
- wymaga zalogowanego uzytkownika,
|
||||
- sprawdza `strtolower($user['email']) === 'biuro@project-pro.pl'`,
|
||||
- pobiera `task_id` jako int i odczytuje zadanie przez `factory\Tasks::task_details`,
|
||||
- odrzuca brak zadania lub pusta tresc komunikatem JSON,
|
||||
- wywoluje `TaskTitleGenerator`,
|
||||
- zwraca JSON `{status: "success", title: "..."}` albo `{status: "error", msg: "..."}`.
|
||||
Endpoint nie moze zapisywac `tasks.name`; zapis pozostaje w istniejacym `/tasks/task_change_title/`.
|
||||
Uzyc medoo/prepared statements przez istniejace metody lub medoo API, bez skladania SQL.
|
||||
</action>
|
||||
<verify>C:\xampp\php\php.exe -l autoload/controls/class.Tasks.php</verify>
|
||||
<done>AC-2, AC-3 i AC-4 spelnione po stronie backendu</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Dodac przycisk i obsluge UI w popupie zadania</name>
|
||||
<files>templates/tasks/task_popup.php</files>
|
||||
<action>
|
||||
Rozszerzyc widok tytulu w popupie:
|
||||
- wyrenderowac przycisk AI tylko gdy `$this->user['email']` to `biuro@project-pro.pl`,
|
||||
- ustawic przycisk obok istniejacego przycisku edycji tytulu,
|
||||
- po kliknieciu pokazac stan ladowania i wywolac `/tasks/task_generate_title/`,
|
||||
- po sukcesie otworzyc inline edycje tytulu i wpisac propozycje do `.task-title-input`,
|
||||
- nie klikac automatycznie `.task-title-save`,
|
||||
- po bledzie pokazac komunikat i zostawic aktualny tytul bez zmian.
|
||||
Zachowac istniejacy mechanizm recznej edycji i zapisu tytulu.
|
||||
Escapowac dane w widoku; nie wprowadzac logiki OpenAI do widoku.
|
||||
</action>
|
||||
<verify>C:\xampp\php\php.exe -l templates/tasks/task_popup.php</verify>
|
||||
<done>AC-1, AC-2 i AC-3 spelnione w UI</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>Przycisk AI w popupie zadania oraz generowanie propozycji tytulu do pola edycji.</what-built>
|
||||
<how-to-verify>
|
||||
1. Zaloguj sie jako `biuro@project-pro.pl`.
|
||||
2. Otworz liste zadan i popup zadania z niepusta trescia.
|
||||
3. Kliknij przycisk AI obok edycji tytulu.
|
||||
4. Potwierdz, ze pole edycji tytulu wypelnia sie propozycja, ale tytul zapisuje sie dopiero po kliknieciu istniejacego przycisku zapisu.
|
||||
5. Sprawdz u innego uzytkownika, ze przycisk nie jest widoczny.
|
||||
</how-to-verify>
|
||||
<resume-signal>Napisz "approved", jesli dziala, albo opisz blad do poprawy.</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- Nie zmieniac istniejacego endpointu `/tasks/task_change_title/` poza ewentualna minimalna walidacja, jesli bedzie konieczna.
|
||||
- Nie zmieniac importu maili w `MailToTaskImporter`.
|
||||
- Nie dodawac nowych bibliotek ani zaleznosci Composer.
|
||||
- Nie zmieniac schematu bazy danych.
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Funkcja dotyczy tylko popupu zadania, nie pelnego formularza `task_edit`.
|
||||
- AI generuje tylko propozycje tytulu; nie generuje ani nie zmienia tresci zadania.
|
||||
- Przycisk jest dostepny tylko dla zalogowanego `biuro@project-pro.pl`.
|
||||
- Brak automatycznego zapisu tytulu po generowaniu.
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] `C:\xampp\php\php.exe -l config.php`
|
||||
- [ ] `C:\xampp\php\php.exe -l autoload/Domain/Tasks/TaskTitleGenerator.php`
|
||||
- [ ] `C:\xampp\php\php.exe -l autoload/controls/class.Tasks.php`
|
||||
- [ ] `C:\xampp\php\php.exe -l templates/tasks/task_popup.php`
|
||||
- [ ] Reczna proba jako `biuro@project-pro.pl`: przycisk widoczny i generuje propozycje do pola.
|
||||
- [ ] Reczna proba jako inny uzytkownik: przycisk niewidoczny, endpoint odrzuca dostep.
|
||||
- [ ] Reczna proba potwierdza, ze baza zmienia tytul dopiero po istniejacym zapisie.
|
||||
- [ ] Wszystkie AC spelnione.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- `biuro@project-pro.pl` moze wygenerowac krotki tytul z tresci zadania w popupie.
|
||||
- Wygenerowany tytul jest tylko propozycja w polu edycji, bez automatycznego zapisu.
|
||||
- Inni uzytkownicy nie widza przycisku i nie moga uzyc endpointu.
|
||||
- Bledy OpenAI nie psuja popupu i nie zmieniaja danych.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Po wykonaniu utworz `.paul/phases/06-task-title-ai/06-01-SUMMARY.md`.
|
||||
</output>
|
||||
144
.paul/phases/06-task-title-ai/06-01-SUMMARY.md
Normal file
144
.paul/phases/06-task-title-ai/06-01-SUMMARY.md
Normal file
@@ -0,0 +1,144 @@
|
||||
---
|
||||
phase: 06-task-title-ai
|
||||
plan: 01
|
||||
subsystem: tasks-ai-ui
|
||||
tags: [tasks, openai, popup, ajax, gpt-5-nano]
|
||||
requires: []
|
||||
provides:
|
||||
- AI title suggestion button in task popup for biuro@project-pro.pl
|
||||
- AJAX endpoint returning title suggestions without saving task name
|
||||
- Domain service for OpenAI task title generation
|
||||
affects: [tasks, openai-integration]
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns: [domain-service-openai, ajax-suggestion-with-explicit-save]
|
||||
key-files:
|
||||
created:
|
||||
- autoload/Domain/Tasks/TaskTitleGenerator.php
|
||||
modified:
|
||||
- config.php
|
||||
- autoload/controls/class.Tasks.php
|
||||
- templates/tasks/task_popup.php
|
||||
key-decisions:
|
||||
- "AI title button visible only for logged-in biuro@project-pro.pl"
|
||||
- "Generated title is inserted into the inline input and saved only by existing save action"
|
||||
- "Task title model defaults to gpt-5-nano"
|
||||
patterns-established:
|
||||
- "OpenAI task-title generation is isolated in Domain\\Tasks\\TaskTitleGenerator"
|
||||
- "Popup AI actions must return suggestions, not persist task data directly"
|
||||
duration: 35min
|
||||
started: 2026-05-04T23:10:00+02:00
|
||||
completed: 2026-05-04T23:45:00+02:00
|
||||
---
|
||||
|
||||
# Phase 6 Plan 01: AI Task Title Summary
|
||||
|
||||
AI title suggestions now work in the task popup for `biuro@project-pro.pl`, inserting a short bezosobowy title proposal into the existing title input without automatic database save.
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Duration | ~35min |
|
||||
| Started | 2026-05-04T23:10:00+02:00 |
|
||||
| Completed | 2026-05-04T23:45:00+02:00 |
|
||||
| Tasks | 3 auto tasks + 1 human verification checkpoint |
|
||||
| Files modified | 4 source/config files + PAUL docs |
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Przycisk widoczny tylko dla biuro | Pass | `task_popup.php` renders AI button only when logged-in user email is `biuro@project-pro.pl`. |
|
||||
| AC-2: Brak przycisku dla innych uzytkownikow | Pass | UI condition hides button; backend endpoint checks the same email before calling OpenAI. |
|
||||
| AC-3: AI generuje propozycje bez zapisu do bazy | Pass | `/tasks/task_generate_title/` returns JSON with `title`; JS inserts it into `.task-title-input`; existing `/tasks/task_change_title/` remains the only save path. |
|
||||
| AC-4: Obsluga bledow OpenAI jest czytelna | Pass | Service returns `{status:error,msg}` for missing key, empty task text, cURL/API errors, and empty OpenAI content; UI shows alert and leaves title unchanged. |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Added `Domain\Tasks\TaskTitleGenerator` to encapsulate OpenAI title generation.
|
||||
- Added `$settings['openai_task_title_model'] = 'gpt-5-nano'` for task-title generation independent from email import parsing.
|
||||
- Added `/tasks/task_generate_title/` AJAX endpoint with user/email gate and no write to `tasks.name`.
|
||||
- Added AI button in task popup, loading state, AJAX call, and insertion of the generated suggestion into the existing inline title editor.
|
||||
- Adjusted GPT-5 request for practical output by using `reasoning_effort = minimal` and larger `max_completion_tokens`.
|
||||
- Refined prompt so titles are short, bezosobowe, and rzeczownikowe, e.g. `Usuniecie bloku o firmie`.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `autoload/Domain/Tasks/TaskTitleGenerator.php` | Created | OpenAI client/service for short task title suggestions. |
|
||||
| `config.php` | Modified | Added `openai_task_title_model` defaulting to `gpt-5-nano`. |
|
||||
| `autoload/controls/class.Tasks.php` | Modified | Added `task_generate_title()` endpoint with authorization and JSON response. |
|
||||
| `templates/tasks/task_popup.php` | Modified | Added conditional AI button and client-side generation flow. |
|
||||
| `.paul/phases/06-task-title-ai/06-01-PLAN.md` | Created | Formal PAUL plan for the phase. |
|
||||
| `.paul/phases/06-task-title-ai/06-01-SUMMARY.md` | Created | This execution summary. |
|
||||
|
||||
## Verification Results
|
||||
|
||||
| Check | Result |
|
||||
|-------|--------|
|
||||
| `C:\xampp\php\php.exe -l config.php` | Pass |
|
||||
| `C:\xampp\php\php.exe -l autoload/Domain/Tasks/TaskTitleGenerator.php` | Pass |
|
||||
| `C:\xampp\php\php.exe -l autoload/controls/class.Tasks.php` | Pass |
|
||||
| `C:\xampp\php\php.exe -l templates/tasks/task_popup.php` | Pass |
|
||||
| Manual user verification | Pass after prompt/token fix; user confirmed OK. |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| Use `gpt-5-nano` | User asked for cheap/fast model and approved this model. | Separate setting avoids changing email import behavior. |
|
||||
| Insert suggestion only, do not auto-save | User selected safer UX. | Existing save path remains authoritative. |
|
||||
| Gate by logged-in email | User clarified scope. | Both UI and backend enforce `biuro@project-pro.pl`. |
|
||||
| Use bezosobowy prompt | User requested shorter non-personal titles. | AI output better matches CRM task naming convention. |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Summary
|
||||
|
||||
| Type | Count | Impact |
|
||||
|------|-------|--------|
|
||||
| Auto-fixed | 2 | Necessary fixes for GPT-5 output reliability and title style. |
|
||||
| Scope additions | 0 | No scope creep. |
|
||||
| Deferred | 0 | None. |
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. GPT-5 empty content with low completion limit**
|
||||
- **Found during:** Human verification
|
||||
- **Issue:** OpenAI returned HTTP 200 but no usable `message.content`, resulting in `OpenAI nie zwrocil poprawnego tytulu.`
|
||||
- **Fix:** Added `reasoning_effort = minimal` and raised `max_completion_tokens` to 500 for `gpt-5*` models.
|
||||
- **Files:** `autoload/Domain/Tasks/TaskTitleGenerator.php`
|
||||
- **Verification:** `php -l` passed; user continued testing.
|
||||
|
||||
**2. Prompt title style too broad**
|
||||
- **Found during:** Human verification
|
||||
- **Issue:** User wanted bezosobowy, shortened titles such as `Usuniecie bloku o firmie`.
|
||||
- **Fix:** Prompt now requires max 6 words, noun-like bezosobowa form, no imperative verbs.
|
||||
- **Files:** `autoload/Domain/Tasks/TaskTitleGenerator.php`
|
||||
- **Verification:** `php -l` passed; user confirmed OK.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
| Issue | Resolution |
|
||||
|-------|------------|
|
||||
| `apply_patch` could not update files with legacy encoding reliably | Used PowerShell with project file encoding for existing files; new PHP service was rewritten UTF-8 without BOM after `php -l` caught BOM before namespace. |
|
||||
| Initial CSS insertion in popup selector block was malformed | Corrected CSS selector block and reran `php -l`. |
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- Phase 6 is complete and can be used from the task popup.
|
||||
- OpenAI title generation is isolated and configurable.
|
||||
- Phase 2 critical bug fixes can resume next.
|
||||
|
||||
**Concerns:**
|
||||
- Existing OpenAI API key remains in `config.php`; this was pre-existing and not changed except for adding the model setting.
|
||||
|
||||
**Blockers:**
|
||||
- None.
|
||||
|
||||
---
|
||||
*Phase: 06-task-title-ai, Plan: 01*
|
||||
*Completed: 2026-05-04*
|
||||
190
autoload/Domain/Tasks/TaskTitleGenerator.php
Normal file
190
autoload/Domain/Tasks/TaskTitleGenerator.php
Normal file
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
namespace Domain\Tasks;
|
||||
|
||||
class TaskTitleGenerator
|
||||
{
|
||||
private const ENDPOINT = 'https://api.openai.com/v1/chat/completions';
|
||||
private const CONTENT_LIMIT = 3000;
|
||||
private const TITLE_LIMIT = 120;
|
||||
|
||||
public function generate( $api_key, $model, $task_text )
|
||||
{
|
||||
$api_key = trim( (string)$api_key );
|
||||
$model = trim( (string)$model );
|
||||
$task_text = $this -> prepareTaskText( $task_text );
|
||||
|
||||
if ( $api_key === '' )
|
||||
return $this -> error( 'Brak klucza API OpenAI.' );
|
||||
|
||||
if ( $model === '' )
|
||||
$model = 'gpt-5-nano';
|
||||
|
||||
if ( $task_text === '' )
|
||||
return $this -> error( 'Brak tresci zadania do wygenerowania tytulu.' );
|
||||
|
||||
if ( !function_exists( 'curl_init' ) )
|
||||
return $this -> error( 'Brak rozszerzenia cURL.' );
|
||||
|
||||
$payload = $this -> buildPayload( $model, $task_text );
|
||||
$response = $this -> sendRequest( $api_key, $payload );
|
||||
if ( $response['status'] !== 'success' )
|
||||
return $response;
|
||||
|
||||
$title = $this -> extractTitle( $response['body'] );
|
||||
if ( $title === '' )
|
||||
return $this -> error( 'OpenAI nie zwrocil poprawnego tytulu.' );
|
||||
|
||||
return [
|
||||
'status' => 'success',
|
||||
'title' => $title
|
||||
];
|
||||
}
|
||||
|
||||
private function buildPayload( $model, $task_text )
|
||||
{
|
||||
$payload = [
|
||||
'model' => $model,
|
||||
'messages' => [
|
||||
[
|
||||
'role' => 'system',
|
||||
'content' => 'Tworzysz bardzo krotkie, bezosobowe tytuly zadan w CRM. Odpowiadasz tylko tytulem, bez komentarza.'
|
||||
],
|
||||
[
|
||||
'role' => 'user',
|
||||
'content' => "Na podstawie tresci zadania zaproponuj jeden skrocony tytul po polsku.\n" .
|
||||
"Wymagania:\n" .
|
||||
"- maksymalnie 6 slow,\n" .
|
||||
"- forma bezosobowa rzeczownikowa, np. \"Usuniecie bloku o firmie\",\n" .
|
||||
"- bez trybu rozkazujacego i bez form typu \"usun\", \"dodaj\", \"popraw\",\n" .
|
||||
"- bez cudzyslowow,\n" .
|
||||
"- bez kropki na koncu,\n" .
|
||||
"- bez ogolnikow typu \"zadanie\" lub \"sprawa\".\n\n" .
|
||||
"Tresc zadania:\n" . $task_text
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
if ( stripos( $model, 'gpt-5' ) === 0 )
|
||||
{
|
||||
$payload['reasoning_effort'] = 'minimal';
|
||||
$payload['max_completion_tokens'] = 500;
|
||||
}
|
||||
else
|
||||
{
|
||||
$payload['temperature'] = 0.2;
|
||||
$payload['max_tokens'] = 80;
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
private function sendRequest( $api_key, array $payload )
|
||||
{
|
||||
$ch = curl_init( self::ENDPOINT );
|
||||
curl_setopt_array( $ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode( $payload ),
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $api_key
|
||||
],
|
||||
CURLOPT_TIMEOUT => 20
|
||||
] );
|
||||
|
||||
$body = curl_exec( $ch );
|
||||
$curl_error = curl_error( $ch );
|
||||
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
|
||||
curl_close( $ch );
|
||||
|
||||
if ( $body === false )
|
||||
return $this -> error( 'Blad polaczenia z OpenAI: ' . $curl_error );
|
||||
|
||||
if ( (int)$http_code !== 200 )
|
||||
return $this -> error( 'OpenAI zwrocil blad HTTP ' . (int)$http_code . ': ' . $this -> extractApiErrorMessage( $body ) );
|
||||
|
||||
if ( trim( (string)$body ) === '' )
|
||||
return $this -> error( 'Pusta odpowiedz z OpenAI.' );
|
||||
|
||||
return [
|
||||
'status' => 'success',
|
||||
'body' => (string)$body
|
||||
];
|
||||
}
|
||||
|
||||
private function extractTitle( $response_body )
|
||||
{
|
||||
$data = json_decode( (string)$response_body, true );
|
||||
if ( !is_array( $data ) )
|
||||
return '';
|
||||
|
||||
if ( isset( $data['error']['message'] ) )
|
||||
return '';
|
||||
|
||||
$content = '';
|
||||
if ( isset( $data['choices'][0]['message']['content'] ) )
|
||||
$content = $this -> normalizeContent( $data['choices'][0]['message']['content'] );
|
||||
|
||||
$content = trim( preg_replace( '/\s+/', ' ', $content ) );
|
||||
$content = trim( $content, " \t\n\r\0\x0B\"'`." );
|
||||
|
||||
if ( $content === '' )
|
||||
return '';
|
||||
|
||||
return mb_substr( $content, 0, self::TITLE_LIMIT );
|
||||
}
|
||||
|
||||
private function normalizeContent( $content )
|
||||
{
|
||||
if ( is_string( $content ) )
|
||||
return $content;
|
||||
|
||||
if ( !is_array( $content ) )
|
||||
return '';
|
||||
|
||||
$parts = [];
|
||||
foreach ( $content as $item )
|
||||
{
|
||||
if ( is_string( $item ) )
|
||||
$parts[] = $item;
|
||||
elseif ( is_array( $item ) && isset( $item['text'] ) && is_string( $item['text'] ) )
|
||||
$parts[] = $item['text'];
|
||||
elseif ( is_array( $item ) && isset( $item['text']['value'] ) && is_string( $item['text']['value'] ) )
|
||||
$parts[] = $item['text']['value'];
|
||||
}
|
||||
|
||||
return implode( ' ', $parts );
|
||||
}
|
||||
|
||||
private function prepareTaskText( $task_text )
|
||||
{
|
||||
$task_text = html_entity_decode( (string)$task_text, ENT_QUOTES, 'UTF-8' );
|
||||
$task_text = preg_replace( '/<script\b[^>]*>.*?<\/script>/is', ' ', $task_text );
|
||||
$task_text = preg_replace( '/<style\b[^>]*>.*?<\/style>/is', ' ', $task_text );
|
||||
$task_text = strip_tags( $task_text );
|
||||
$task_text = preg_replace( '/\s+/', ' ', $task_text );
|
||||
$task_text = trim( $task_text );
|
||||
|
||||
if ( $task_text === '' )
|
||||
return '';
|
||||
|
||||
return mb_substr( $task_text, 0, self::CONTENT_LIMIT );
|
||||
}
|
||||
|
||||
private function extractApiErrorMessage( $response_body )
|
||||
{
|
||||
$data = json_decode( (string)$response_body, true );
|
||||
if ( is_array( $data ) && isset( $data['error']['message'] ) )
|
||||
return (string)$data['error']['message'];
|
||||
|
||||
return mb_substr( trim( (string)$response_body ), 0, 300 );
|
||||
}
|
||||
|
||||
private function error( $message )
|
||||
{
|
||||
return [
|
||||
'status' => 'error',
|
||||
'msg' => (string)$message
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -372,6 +372,51 @@ class Tasks
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function task_generate_title()
|
||||
{
|
||||
global $user, $settings;
|
||||
|
||||
$response = [ 'status' => 'error', 'msg' => 'Nie udało się wygenerować tytułu zadania.' ];
|
||||
|
||||
if ( !$user or !isset( $user['email'] ) or strtolower( trim( (string)$user['email'] ) ) !== 'biuro@project-pro.pl' )
|
||||
{
|
||||
echo json_encode( [ 'status' => 'error', 'msg' => 'Brak uprawnień do generowania tytułu.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$task_id = (int)\S::get( 'task_id' );
|
||||
if ( !$task_id )
|
||||
{
|
||||
echo json_encode( [ 'status' => 'error', 'msg' => 'Nieprawidłowe zadanie.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$task = \factory\Tasks::task_details( $task_id, (int)$user['id'] );
|
||||
if ( !is_array( $task ) or !isset( $task['id'] ) )
|
||||
{
|
||||
echo json_encode( [ 'status' => 'error', 'msg' => 'Nie znaleziono zadania.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$task_text = isset( $task['text'] ) ? trim( (string)$task['text'] ) : '';
|
||||
if ( $task_text === '' )
|
||||
{
|
||||
echo json_encode( [ 'status' => 'error', 'msg' => 'Zadanie nie ma treści do wygenerowania tytułu.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$api_key = isset( $settings['openai_api_key'] ) ? trim( (string)$settings['openai_api_key'] ) : '';
|
||||
$model = isset( $settings['openai_task_title_model'] ) ? trim( (string)$settings['openai_task_title_model'] ) : 'gpt-5-nano';
|
||||
$generator = new \Domain\Tasks\TaskTitleGenerator();
|
||||
$result = $generator -> generate( $api_key, $model, $task_text );
|
||||
|
||||
if ( is_array( $result ) and isset( $result['status'] ) )
|
||||
$response = $result;
|
||||
|
||||
echo json_encode( $response );
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function task_change_text() {
|
||||
global $mdb;
|
||||
|
||||
|
||||
@@ -23,3 +23,5 @@ $imap_tasks['password'] = 'ProjectPro2025!';
|
||||
$settings['openai_api_key'] = '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'; // Wklej tutaj swój klucz API OpenAI
|
||||
$settings['openai_parse_emails'] = false; // true = użyj AI do parsowania emaili, false = normalne parsowanie
|
||||
$settings['openai_model'] = 'gpt-4o-mini'; // Model: gpt-4o-mini, gpt-4o, gpt-5-nano, itp.
|
||||
$settings['openai_task_title_model'] = 'gpt-5-nano'; // Tani i szybki model do generowania tytulow zadan
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
<?
|
||||
$can_generate_ai_title = isset( $this -> user['email'] ) && strtolower( trim( (string)$this -> user['email'] ) ) === 'biuro@project-pro.pl';
|
||||
?>
|
||||
<div class="task_details" task_id="<?= $this -> task['id'];?>">
|
||||
<a href="#" class="close"><i class="fa fa-times"></i></a>
|
||||
<div class="title">
|
||||
@@ -12,8 +15,11 @@
|
||||
<span class="task-title-wrapper">
|
||||
<span class="task-id">#<?= $this -> task['id'];?></span>
|
||||
<span class="task-title-view">
|
||||
<span class="task-title-text"><?= $this -> task['name'];?></span>
|
||||
<span class="task-title-text"><?= htmlspecialchars( (string)$this -> task['name'] );?></span>
|
||||
<a href="#" class="task-title-edit-btn" title="Edytuj tytuł"><i class="fa fa-pencil"></i></a>
|
||||
<? if ( $can_generate_ai_title ):?>
|
||||
<a href="#" class="task-title-ai-btn" title="Generuj tytul AI"><i class="fa fa-magic"></i></a>
|
||||
<? endif;?>
|
||||
</span>
|
||||
<span class="task-title-edit-box" style="display: none;">
|
||||
<input type="text" class="task-title-input form-control" value="<?= htmlspecialchars($this -> task['name']);?>">
|
||||
@@ -602,6 +608,7 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
.task_popup .task_details .title .task-title-edit-btn,
|
||||
.task_popup .task_details .title .task-title-ai-btn,
|
||||
.task_popup .task_details .title .task-title-save,
|
||||
.task_popup .task_details .title .task-title-cancel {
|
||||
border: 1px solid #099885;
|
||||
@@ -622,6 +629,18 @@
|
||||
background: #099885;
|
||||
color: #fff;
|
||||
}
|
||||
.task_popup .task_details .title .task-title-ai-btn {
|
||||
border-color: #299cdb;
|
||||
color: #299cdb;
|
||||
}
|
||||
.task_popup .task_details .title .task-title-ai-btn:hover {
|
||||
background: #299cdb;
|
||||
color: #fff;
|
||||
}
|
||||
.task_popup .task_details .title .task-title-ai-btn.disabled {
|
||||
pointer-events: none;
|
||||
opacity: .65;
|
||||
}
|
||||
.task_popup .task_details .title .task-title-cancel {
|
||||
border-color: #cc563d;
|
||||
color: #cc563d;
|
||||
@@ -1025,6 +1044,48 @@
|
||||
popup.find( '.task-title-input' ).focus();
|
||||
});
|
||||
|
||||
popup.on( 'click', '.task-title-ai-btn', function( e ) {
|
||||
e.preventDefault();
|
||||
|
||||
var btn = $( this );
|
||||
var task_id = popup.attr( 'task_id' );
|
||||
var original_html = btn.html();
|
||||
|
||||
if ( btn.hasClass( 'disabled' ) )
|
||||
return;
|
||||
|
||||
btn
|
||||
.addClass( 'disabled' )
|
||||
.html( '<i class="fa fa-spinner fa-spin"></i>' );
|
||||
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/tasks/task_generate_title/',
|
||||
data: { task_id: task_id },
|
||||
success: function( response ) {
|
||||
var res = typeof response === 'string' ? JSON.parse( response ) : response;
|
||||
if ( res.status === 'success' && res.title )
|
||||
{
|
||||
popup.find( '.task-title-view' ).hide();
|
||||
popup.find( '.task-title-edit-box' ).css( 'display', 'inline-flex' );
|
||||
popup.find( '.task-title-input' ).val( res.title ).focus().select();
|
||||
}
|
||||
else
|
||||
{
|
||||
alert( res.msg ? res.msg : 'Nie udalo sie wygenerowac tytulu.' );
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert( 'Nie udalo sie wygenerowac tytulu.' );
|
||||
},
|
||||
complete: function() {
|
||||
btn
|
||||
.removeClass( 'disabled' )
|
||||
.html( original_html );
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
popup.on( 'click', '.task-title-cancel', function( e ) {
|
||||
e.preventDefault();
|
||||
popup.find( '.task-title-edit-box' ).hide();
|
||||
|
||||
Reference in New Issue
Block a user