// OpenRouter-powered commit summary + small avatar helpers. import { p4, OpenedFile, statusOf } from "./p4"; const MODEL_KEY = "exd-ai-model"; const KEY_KEY = "exd-ai-key"; // cheap + reliable default; change to a "…:free" model in Settings to pay nothing export const DEFAULT_MODEL = "openai/gpt-4o-mini"; export function getModel(): string { try { return localStorage.getItem(MODEL_KEY) || DEFAULT_MODEL; } catch { return DEFAULT_MODEL; } } export function setModel(m: string) { try { localStorage.setItem(MODEL_KEY, m.trim() || DEFAULT_MODEL); } catch {} } export function getKeyLocal(): string { try { return localStorage.getItem(KEY_KEY) || ""; } catch { return ""; } } export function setKeyLocal(k: string) { try { localStorage.setItem(KEY_KEY, k.trim()); } catch {} } // key resolution: localStorage first, else the app-config file (git-ignored) export async function resolveKey(): Promise { const local = getKeyLocal(); if (local) return local; try { return (await p4.readAiKey()) || ""; } catch { return ""; } } // generate a commit summary + description from the changed files export async function aiSummary(files: OpenedFile[]): Promise<{ summary: string; description: string }> { const key = await resolveKey(); if (!key) throw new Error("No OpenRouter API key — set it in Settings"); const list = files.slice(0, 200).map((f) => `${statusOf(f.action).ch} ${f.depotFile || ""}`).join("\n"); const res = await fetch("https://openrouter.ai/api/v1/chat/completions", { method: "POST", headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json", "X-Title": "Exbyte Depot" }, body: JSON.stringify({ model: getModel(), max_tokens: 300, temperature: 0.3, messages: [ { role: "system", content: "You write concise commit messages for a Perforce/Unreal project. Given a list of changed files (+ added, ± edited, − deleted), reply with ONE short imperative summary line (under 72 chars, no trailing period), then a blank line, then an optional 1-3 line description. Plain text only — no markdown, no code fences, no quotes." }, { role: "user", content: `Changed files:\n${list}` }, ], }), }); if (!res.ok) { const txt = await res.text().catch(() => ""); throw new Error(`OpenRouter ${res.status}: ${txt.slice(0, 180)}`); } const data = await res.json(); const content: string = (data?.choices?.[0]?.message?.content || "").trim(); if (!content) throw new Error("Empty AI response"); const lines = content.split("\n"); const summary = (lines[0] || "").replace(/^["'`*]+|["'`*]+$/g, "").trim(); const description = lines.slice(1).join("\n").trim(); return { summary, description }; } // --- avatar helpers --- export function initials(name: string, user: string): string { const n = (name || "").trim(); if (n) { const p = n.split(/\s+/); return (p[0][0] + (p[1]?.[0] || "")).toUpperCase(); } return (user || "?").slice(0, 2).toUpperCase(); } export function avatarColor(seed: string): string { let h = 0; for (let i = 0; i < seed.length; i++) h = (h * 31 + seed.charCodeAt(i)) >>> 0; return `hsl(${h % 360} 52% 52%)`; } // activity level from a unix-epoch last-access string export function activity(access?: string): "active" | "recent" | "idle" { const n = Number(access || 0) * 1000; if (!n) return "idle"; const ago = Date.now() - n; if (ago < 60 * 60 * 1000) return "active"; // < 1h if (ago < 24 * 60 * 60 * 1000) return "recent"; // < 1d return "idle"; }