Files
Randify.pro/src/lib/client/translation.ts
T
emilandClaude Opus 4.7 cce582df6c feat(dm-dashboard): Wave 8 — spell generator + Open5e equipment/magicitems + UX polish
Wave 8 closes the high-value gaps from the original spec while staying
inside MVP scope (encounters/weapons/items still future work).

Spell generator (mirrors the NPC pattern end-to-end)
- src/lib/ai/types.ts: spellParamsSchema, spellResultSchema, Open5eSpell
- src/lib/ai/openrouter.ts: generateSpell using llama-3.3-70b:free for FREE
- src/lib/ai/kimi.ts: generateSpell using moonshot-v1-8k with json_schema
  response_format for PRO
- src/pages/api/dm/ai/generate-spell.ts: auth → rate-limit → optional
  /spells/ Open5e reference for balance → AI generate → Zod validate →
  increment counter. Spells are returned to client and not persisted
  (spec only requires NPC persistence for PRO).
- src/components/dm/AiSpellForm.astro: form (level, school, classes,
  tone, suggested name) + inline result card rendering with copy-JSON
- src/components/dm/AiKindSwitcher.astro: segmented NPC | Spell control
  wrapping both forms; default tab is NPC

Open5e Reference: equipment + magic items tabs
- src/lib/open5e/client.ts: searchEquipment, getEquipmentItem,
  searchMagicItems, getMagicItem (plus EquipmentItem / MagicItem types)
- src/lib/client/open5e-ui.ts: Tab type extended to four, search() and
  selectItem() dispatch all four
- src/components/dm/Open5eReference.astro: two more tab buttons; new
  renderEquipmentCard/Detail and renderMagicItemCard/Detail; filter
  dropdown hidden for new tabs (search-by-name only)
- src/lib/client/translation.ts: TranslationType union now includes
  "equipment" | "magicitem"
- src/lib/ai/openrouter.ts: translateOpen5eContent type widened (export
  Open5eContentType)
- src/pages/api/dm/translate.ts: ALLOWED_TYPES adds equipment/magicitem
  and dispatches to the right Open5e fetcher

UX polish
- src/components/dm/AiQuotaExhausted.astro: dedicated state shown to
  authenticated FREE users with remaining=0 — replaces the form with
  reset-time message and Boosty upgrade hint
- src/pages/{dm,ru/dm}/index.astro: render AiQuotaExhausted when the
  guard passes; otherwise render the kind switcher
- src/middleware/index.ts: remove [Middleware] debug console.logs that
  were noisy in prod logs (one line per request)

Verified locally: tsc 0 errors, lint 0 errors, vitest 339/339 passing,
npm run build succeeds without auth env vars.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 02:47:41 +03:00

79 lines
1.8 KiB
TypeScript

export type TranslationType = "creature" | "spell" | "equipment" | "magicitem";
export interface TranslationEntry {
slug: string;
type: TranslationType;
data: Record<string, unknown>;
timestamp: number;
}
const CACHE = new Map<string, TranslationEntry>();
const CHANNEL =
typeof BroadcastChannel !== "undefined"
? new BroadcastChannel("dm-translations")
: null;
if (CHANNEL) {
CHANNEL.addEventListener("message", (e) => {
const entry = e.data as TranslationEntry;
CACHE.set(`${entry.type}:${entry.slug}`, entry);
});
}
export function getCachedTranslation(
slug: string,
type: TranslationType,
): Record<string, unknown> | null {
return CACHE.get(`${type}:${slug}`)?.data ?? null;
}
export function setCachedTranslation(
slug: string,
type: TranslationType,
data: Record<string, unknown>,
): void {
const entry: TranslationEntry = {
slug,
type,
data,
timestamp: Date.now(),
};
CACHE.set(`${type}:${slug}`, entry);
CHANNEL?.postMessage(entry);
}
export async function fetchTranslation(
slug: string,
type: TranslationType,
): Promise<Record<string, unknown> | null> {
const cached = getCachedTranslation(slug, type);
if (cached) return cached;
const res = await fetch(
`/api/dm/translate?slug=${encodeURIComponent(slug)}&type=${type}`,
);
const data = (await res.json().catch(() => ({}))) as {
error?: string;
translated?: Record<string, unknown>;
};
if (!res.ok || data.error) {
throw new Error(data.error || "Translation failed");
}
const translated = data.translated ?? null;
if (translated) {
setCachedTranslation(slug, type, translated);
}
return translated;
}
export function clearTranslationCache(): void {
CACHE.clear();
}
export function getTranslationCacheSize(): number {
return CACHE.size;
}