Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdbe48ce1a | ||
|
|
87c541b178 |
@@ -99,12 +99,17 @@ func (s *Server) handleAdminTextGenChapterNames(w http.ResponseWriter, r *http.R
|
||||
}
|
||||
|
||||
systemPrompt := `You are a chapter title editor for a web novel platform. ` +
|
||||
`The user will provide a list of chapter numbers with their current titles, ` +
|
||||
`and a naming pattern. Your task is to produce a renamed version of every chapter ` +
|
||||
`following the pattern exactly. ` +
|
||||
`Respond ONLY with a JSON array — no prose, no markdown fences, no explanation. ` +
|
||||
`Each element must be an object: {"number": <int>, "title": <string>}. ` +
|
||||
`Output every chapter in the input list. Do not skip any.`
|
||||
`The user provides a list of chapter numbers with their current titles, ` +
|
||||
`and a naming pattern template. ` +
|
||||
`Your job: produce one new title for every chapter, following the pattern exactly. ` +
|
||||
`Pattern placeholders: {n} = the chapter number (integer), {scene} = a very short (2–5 word) scene hint derived from the existing title. ` +
|
||||
`RULES: ` +
|
||||
`1. Do NOT include the chapter number inside the title text — the {n} placeholder is already in the pattern. ` +
|
||||
`2. Do NOT include any prefix like "Chapter X -" or "Chapter X:" inside the title field itself. ` +
|
||||
`3. The "title" field in your JSON must be the fully-rendered string (e.g. if pattern is "Chapter {n}: {scene}", output "Chapter 3: The Bet"). ` +
|
||||
`4. Respond ONLY with a raw JSON array — no prose, no markdown fences, no explanation. ` +
|
||||
`5. Each element: {"number": <int>, "title": <string>}. ` +
|
||||
`6. Output every chapter in the input list, in order. Do not skip any.`
|
||||
|
||||
userPrompt := fmt.Sprintf(
|
||||
"Naming pattern: %s\n\nChapters:\n%s",
|
||||
@@ -117,8 +122,14 @@ func (s *Server) handleAdminTextGenChapterNames(w http.ResponseWriter, r *http.R
|
||||
model = cfai.DefaultTextModel
|
||||
}
|
||||
|
||||
// Default to 4096 tokens so large chapter lists are not truncated.
|
||||
maxTokens := req.MaxTokens
|
||||
if maxTokens <= 0 {
|
||||
maxTokens = 4096
|
||||
}
|
||||
|
||||
s.deps.Log.Info("admin: text-gen chapter-names requested",
|
||||
"slug", req.Slug, "chapters", len(chapters), "model", model)
|
||||
"slug", req.Slug, "chapters", len(chapters), "model", model, "max_tokens", maxTokens)
|
||||
|
||||
raw, genErr := s.deps.TextGen.Generate(r.Context(), cfai.TextRequest{
|
||||
Model: model,
|
||||
@@ -126,7 +137,7 @@ func (s *Server) handleAdminTextGenChapterNames(w http.ResponseWriter, r *http.R
|
||||
{Role: "system", Content: systemPrompt},
|
||||
{Role: "user", Content: userPrompt},
|
||||
},
|
||||
MaxTokens: req.MaxTokens,
|
||||
MaxTokens: maxTokens,
|
||||
})
|
||||
if genErr != nil {
|
||||
s.deps.Log.Error("admin: text-gen chapter-names failed", "err", genErr)
|
||||
|
||||
@@ -192,6 +192,50 @@
|
||||
return () => clearTimeout(id);
|
||||
});
|
||||
|
||||
// ── MediaSession action handlers ────────────────────────────────────────────
|
||||
// Without explicit handlers, iOS Safari loses lock-screen resume ability after
|
||||
// ~1 minute of pause because it falls back to its own default which doesn't
|
||||
// satisfy the user-gesture requirement for <audio>.play().
|
||||
// Handlers registered here call audioEl directly so iOS trusts the gesture.
|
||||
$effect(() => {
|
||||
if (typeof navigator === 'undefined' || !('mediaSession' in navigator) || !audioEl) return;
|
||||
const el = audioEl; // capture for closure
|
||||
|
||||
navigator.mediaSession.setActionHandler('play', () => {
|
||||
el.play().catch(() => {});
|
||||
});
|
||||
navigator.mediaSession.setActionHandler('pause', () => {
|
||||
el.pause();
|
||||
});
|
||||
navigator.mediaSession.setActionHandler('seekbackward', (d) => {
|
||||
el.currentTime = Math.max(0, el.currentTime - (d.seekOffset ?? 15));
|
||||
});
|
||||
navigator.mediaSession.setActionHandler('seekforward', (d) => {
|
||||
el.currentTime = Math.min(el.duration || 0, el.currentTime + (d.seekOffset ?? 30));
|
||||
});
|
||||
// previoustrack / nexttrack fall back to skip ±30s if no chapter nav available
|
||||
try {
|
||||
navigator.mediaSession.setActionHandler('previoustrack', () => {
|
||||
el.currentTime = Math.max(0, el.currentTime - 30);
|
||||
});
|
||||
navigator.mediaSession.setActionHandler('nexttrack', () => {
|
||||
el.currentTime = Math.min(el.duration || 0, el.currentTime + 30);
|
||||
});
|
||||
} catch { /* some browsers don't support these */ }
|
||||
|
||||
return () => {
|
||||
(['play', 'pause', 'seekbackward', 'seekforward'] as MediaSessionAction[]).forEach((a) => {
|
||||
try { navigator.mediaSession.setActionHandler(a, null); } catch { /* ignore */ }
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
// Keep playbackState in sync so iOS lock screen shows the right button state
|
||||
$effect(() => {
|
||||
if (typeof navigator === 'undefined' || !('mediaSession' in navigator)) return;
|
||||
navigator.mediaSession.playbackState = audioStore.isPlaying ? 'playing' : 'paused';
|
||||
});
|
||||
|
||||
// ── Save audio time on pause/end (debounced 2s) ─────────────────────────
|
||||
let audioTimeSaveTimer = 0;
|
||||
function saveAudioTime() {
|
||||
|
||||
@@ -71,6 +71,11 @@
|
||||
);
|
||||
chRawResponse = body.raw_response ?? '';
|
||||
chUsedModel = body.model ?? '';
|
||||
// If backend returned chapters:[] but we have a raw response, the model
|
||||
// output was unparseable (likely truncated). Treat it as an error.
|
||||
if (chProposals.length === 0 && chRawResponse.trim().length > 0) {
|
||||
chError = 'Model response could not be parsed (output may be truncated). Raw response shown below.';
|
||||
}
|
||||
} catch {
|
||||
chError = 'Network error.';
|
||||
} finally {
|
||||
@@ -287,6 +292,9 @@
|
||||
|
||||
{#if chError}
|
||||
<p class="text-sm text-(--color-danger) bg-(--color-danger)/10 rounded-lg px-3 py-2">{chError}</p>
|
||||
{#if chRawResponse}
|
||||
<pre class="text-xs bg-(--color-surface-2) border border-(--color-border) rounded-lg p-3 overflow-auto max-h-48 text-(--color-muted) whitespace-pre-wrap break-words">{chRawResponse}</pre>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user