62d6f9d4d5
build / build (push) Failing after 2s
Testy / Testy warstwy logicznej (silnik) (push) Failing after 4s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Failing after 3s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Failing after 3s
Testy / Testy astrodemo (push) Failing after 3s
Testy / Build obrazu silnika B (swisseph) (push) Failing after 2s
Testy / Kontrola składni wszystkich warstw (push) Failing after 3s
Drugi z pięciu kroków budowy trzech produktów. Czysty refaktor — zachowanie
aplikacji się nie zmienia, liczba testów rośnie tylko o nowe.
DLACZEGO WARUNEK W SZABLONIE NIE WYSTARCZA. Dotąd generowanie było chowane przez
`{% if can(request, 'ai') %}`. To sprawia, że funkcji nie WIDAĆ, ale nie że jej
NIE MA: plik szablonu dalej leży w obrazie i dalej zawiera jej nazwy, więc `grep`
po kontenerze pokazuje wszystko, czego warunek nie pokazał na ekranie. Astroklient
ma nie mieć śladu, nie mieć wyłączoną funkcję — więc granica musi być KATALOGIEM.
CO SIĘ PRZENIOSŁO do app/dodatki/: dwa szablony, pięć plików statycznych, trasa
strumienia, cztery metody klienta warstwy logicznej, wpis w katalogu funkcji,
wpisy tras i zasobów, akcje formularza i reguły CSS nazywające funkcję
(`textarea.prompt`).
JAK APLIKACJA O TO PYTA. Neutralny most `app/rozszerzenia.py` odpowiada wyłącznie
na pytanie „czy coś jest podpięte i co wnosi". Sam nie wymienia ani jednej nazwy —
pilnuje tego osobny test, bo most jest w KAŻDYM obrazie. Dlatego też katalog
nazywa się `dodatki`, a nie `ai`: nazwa w instrukcji importu byłaby dokładnie tym
śladem, którego wydzielanie ma się pozbyć.
TRZY MIEJSCA, KTÓRE OKAZAŁY SIĘ TRUDNE:
1. Pola formularza. FastAPI czyta je z SYGNATURY, a wspólny handler nie może
wymieniać `prompt_budget` ani `llm_provider`. Rozwiązane zależnością: moduł
deklaruje własne pola u siebie, handler wie tylko, że dostaje słownik.
2. Klient warstwy logicznej jest wspólny, więc metody `prompt`/`horoscope`/
`llm_models` musiały z niego wyjść. Wspólny klient daje teraz samą drogę
w dół (`wywolaj`, `pobierz`, `strumien`) — z szyfrowaniem łącza i tokenem
międzywarstwowym; co nią pojedzie, jest sprawą modułu.
3. Podtytuł ekranu Skompiluj miał wariant „z AI" i wariant bez. Zamiast warunku
jedno zdanie prawdziwe niezależnie od tego, jakie moduły są w obrazie.
DOCKERFILE. `COPY . .` wnosiło do obrazu także testy — a plik testowy nazywa
funkcje wprost. Teraz wchodzi wyłącznie `app/`.
TEST GRANICY. `test_ai_tylko_w_module.py` przechodzi po wszystkim poza modułem
i szuka siedemnastu słów. Znalazł dwanaście resztek, których nie widziałem:
komentarze w main.py i security.py, przykład `models.js` w komentarzu features.py,
komentarze w compile.js i w trzech arkuszach, oraz regułę `textarea.prompt`,
która przy rozbijaniu CSS trafiła do arkuszy ekranów zamiast do modułu.
Ma też kontrolę pozytywną (te słowa MAJĄ padać w module — inaczej test
przechodziłby, gdyby funkcję wydrążono) i test odłączalności mostu.
SPRAWDZONE NA KOPII BEZ MODUŁU: aplikacja startuje, wszystkie ekrany oddają 200,
katalog ma 13 funkcji zamiast 14, trasy strumienia nie ma, spreparowane
`action=prompt` wraca do akcji domyślnej bez śladu, a `grep` po drzewie nie
znajduje ani jednego ze słów.
Testy: presentation 362, logic 342, data 37, render 41.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
59 lines
2.2 KiB
JavaScript
59 lines
2.2 KiB
JavaScript
// Podpowiedzi modeli zależne od wybranego dostawcy.
|
|
//
|
|
// Pole modelu jest CELOWO tekstowe (input + datalist), a nie zamkniętym <select>:
|
|
// katalog to tylko wygoda, a konto może mieć dostęp do modeli, o których kod nie
|
|
// wie. Puste pole = model domyślny dostawcy.
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
const providerEl = document.getElementById('llmProvider');
|
|
const modelEl = document.getElementById('llmModel');
|
|
const listEl = document.getElementById('llmModelList');
|
|
const hintEl = document.getElementById('llmModelHint');
|
|
const dataEl = document.getElementById('llmCatalog');
|
|
if (!providerEl || !modelEl || !listEl || !dataEl) return;
|
|
|
|
let catalog = {};
|
|
let defaults = {};
|
|
try {
|
|
const parsed = JSON.parse(dataEl.textContent || '{}');
|
|
catalog = parsed.providers || {};
|
|
defaults = parsed.defaults || {};
|
|
} catch (e) {
|
|
return; // brak katalogu — pole nadal działa jako wolny tekst
|
|
}
|
|
|
|
const fmt = n =>
|
|
n >= 1000000 ? (n / 1000000) + 'M' : (n >= 1000 ? Math.round(n / 1000) + 'k' : String(n));
|
|
|
|
function refresh(resetValue) {
|
|
const provider = providerEl.value;
|
|
const models = catalog[provider] || [];
|
|
|
|
listEl.innerHTML = '';
|
|
models.forEach(m => {
|
|
const opt = document.createElement('option');
|
|
opt.value = m.id;
|
|
opt.label = m.label + ' · kontekst ' + fmt(m.context_window);
|
|
listEl.appendChild(opt);
|
|
});
|
|
|
|
const fallback = defaults[provider] || '';
|
|
modelEl.placeholder = fallback ? 'domyślny: ' + fallback : 'domyślny dostawcy';
|
|
|
|
// po zmianie dostawcy stary model nie ma sensu (np. gpt-4o u Anthropica)
|
|
if (resetValue) modelEl.value = '';
|
|
|
|
if (hintEl) {
|
|
const chosen = models.find(m => m.id === modelEl.value.trim());
|
|
hintEl.textContent = chosen
|
|
? chosen.label + ' — okno kontekstu ' + fmt(chosen.context_window) +
|
|
' tokenów, maksymalna odpowiedź ' + fmt(chosen.max_output) + '.'
|
|
: 'Zostaw puste, by użyć modelu domyślnego. Możesz też wpisać dowolny ' +
|
|
'identyfikator modelu, do którego Twoje konto ma dostęp.';
|
|
}
|
|
}
|
|
|
|
providerEl.addEventListener('change', () => refresh(true));
|
|
modelEl.addEventListener('input', () => refresh(false));
|
|
refresh(false);
|
|
});
|