update see calss

This commit is contained in:
2026-08-23 22:03:57 +02:00
parent f3d4fb56ed
commit 8983cfc8a1
32 changed files with 1884 additions and 22 deletions
+6
View File
@@ -32,6 +32,12 @@ export const ADMIN_NAV_ROUTES: readonly AdminNavRoute[] = [
group: "ops",
fullAdminOnly: true
},
{
titleKey: "admin.nav.aiCalls",
href: "/admin/ai-calls",
group: "ops",
fullAdminOnly: true
},
{
titleKey: "admin.nav.stuckProducts",
href: "/admin/stuck-products",
@@ -29,6 +29,7 @@
Activity,
ArrowLeft,
FileWarning,
MessagesSquare,
LogOut,
X
} from "@lucide/svelte";
@@ -40,6 +41,7 @@
"/admin/support": LifeBuoy,
"/admin/support/knowledge": BookOpen,
"/admin/diagnostics": Activity,
"/admin/ai-calls": MessagesSquare,
"/admin/stuck-products": ClipboardList,
"/admin/orphan-processed": FileWarning,
"/admin/billing": CreditCard,
+37 -1
View File
@@ -5705,5 +5705,41 @@ export const de: MessageDict = {
"settings.permissions.role.viewer.desc": "Dashboard, Katalog und Feeds ansehen. Sonst nichts.",
"settings.permissions.role.custom": "Benutzerdefiniert",
"settings.permissions.role.custom.desc": "Sie haben Bereiche einzeln gewählt — das entspricht keiner Standardrolle.",
"settings.permissions.advanced": "Einzelne Bereiche feinjustieren"
"settings.permissions.advanced": "Einzelne Bereiche feinjustieren",
"admin.nav.aiCalls": "KI-Aufrufe",
"admin.aiCalls.title": "KI-Aufrufe",
"admin.aiCalls.description": "Der exakte Prompt und die Antwort jedes LLM-Aufrufs. {days} Tage aufbewahrt.",
"admin.aiCalls.filtersTitle": "Filter",
"admin.aiCalls.filtersHint": "Firma waehlen, dann nach Benutzer, Auftrag, Rolle oder Ergebnis eingrenzen.",
"admin.aiCalls.company": "Firma",
"admin.aiCalls.allCompanies": "Alle Firmen",
"admin.aiCalls.role": "Rolle",
"admin.aiCalls.outcome": "Ergebnis",
"admin.aiCalls.outcomeAll": "Alle",
"admin.aiCalls.outcomeOk": "Erfolgreich",
"admin.aiCalls.outcomeFailed": "Fehlgeschlagen",
"admin.aiCalls.user": "Benutzer",
"admin.aiCalls.allUsers": "Alle Benutzer",
"admin.aiCalls.jobId": "Auftrags-ID",
"admin.aiCalls.search": "Suche",
"admin.aiCalls.searchHint": "Text in Prompt oder Antwort",
"admin.aiCalls.rowCount": "{count} Aufrufe angezeigt",
"admin.aiCalls.emptyTitle": "Keine passenden KI-Aufrufe",
"admin.aiCalls.emptyBody": "Aufrufe werden {days} Tage aufbewahrt. Auftrag starten und aktualisieren.",
"admin.aiCalls.colWhen": "Wann",
"admin.aiCalls.colCompany": "Firma",
"admin.aiCalls.colUser": "Benutzer",
"admin.aiCalls.colRole": "Rolle",
"admin.aiCalls.colOutcome": "Ergebnis",
"admin.aiCalls.colPrompt": "Prompt",
"admin.aiCalls.colTokens": "Token",
"admin.aiCalls.colTime": "Zeit",
"admin.aiCalls.loadMore": "Aeltere laden",
"admin.aiCalls.detailTitle": "Aufrufdetails",
"admin.aiCalls.systemPrompt": "System-Prompt",
"admin.aiCalls.userPrompt": "Benutzer-Prompt",
"admin.aiCalls.response": "Antwort",
"admin.aiCalls.loadFailed": "KI-Aufrufe konnten nicht geladen werden.",
"admin.aiCalls.copyFailed": "Kopieren fehlgeschlagen.",
"common.copy": "Kopieren",
};
+37 -1
View File
@@ -5791,5 +5791,41 @@ export const en: MessageDict = {
"settings.permissions.role.viewer.desc": "Browse the dashboard, catalog and feeds. Nothing else.",
"settings.permissions.role.custom": "Custom",
"settings.permissions.role.custom.desc": "You picked areas by hand, so this matches no standard role.",
"settings.permissions.advanced": "Fine-tune individual areas"
"settings.permissions.advanced": "Fine-tune individual areas",
"admin.nav.aiCalls": "AI calls",
"admin.aiCalls.title": "AI calls",
"admin.aiCalls.description": "The exact prompt and response of every LLM call. Kept for {days} days.",
"admin.aiCalls.filtersTitle": "Filters",
"admin.aiCalls.filtersHint": "Pick a company, then narrow by user, job, role or outcome.",
"admin.aiCalls.company": "Company",
"admin.aiCalls.allCompanies": "All companies",
"admin.aiCalls.role": "Role",
"admin.aiCalls.outcome": "Outcome",
"admin.aiCalls.outcomeAll": "All",
"admin.aiCalls.outcomeOk": "Succeeded",
"admin.aiCalls.outcomeFailed": "Failed",
"admin.aiCalls.user": "User",
"admin.aiCalls.allUsers": "All users",
"admin.aiCalls.jobId": "Job ID",
"admin.aiCalls.search": "Search",
"admin.aiCalls.searchHint": "Text in prompt or response",
"admin.aiCalls.rowCount": "{count} calls shown",
"admin.aiCalls.emptyTitle": "No AI calls match",
"admin.aiCalls.emptyBody": "Captured calls are kept for {days} days. Run a job, then refresh.",
"admin.aiCalls.colWhen": "When",
"admin.aiCalls.colCompany": "Company",
"admin.aiCalls.colUser": "User",
"admin.aiCalls.colRole": "Role",
"admin.aiCalls.colOutcome": "Outcome",
"admin.aiCalls.colPrompt": "Prompt",
"admin.aiCalls.colTokens": "Tokens",
"admin.aiCalls.colTime": "Time",
"admin.aiCalls.loadMore": "Load older",
"admin.aiCalls.detailTitle": "Call detail",
"admin.aiCalls.systemPrompt": "System prompt",
"admin.aiCalls.userPrompt": "User prompt",
"admin.aiCalls.response": "Response",
"admin.aiCalls.loadFailed": "Could not load AI calls.",
"admin.aiCalls.copyFailed": "Could not copy to clipboard.",
"common.copy": "Copy",
};
+37 -1
View File
@@ -5714,5 +5714,41 @@ export const es: MessageDict = {
"settings.permissions.role.viewer.desc": "Ver el panel, el catálogo y los feeds. Nada más.",
"settings.permissions.role.custom": "Personalizado",
"settings.permissions.role.custom.desc": "Has elegido las secciones a mano, así que no coincide con ningún rol estándar.",
"settings.permissions.advanced": "Ajustar secciones concretas"
"settings.permissions.advanced": "Ajustar secciones concretas",
"admin.nav.aiCalls": "Llamadas de IA",
"admin.aiCalls.title": "Llamadas de IA",
"admin.aiCalls.description": "El prompt y la respuesta exactos de cada llamada al LLM. Se guardan {days} dias.",
"admin.aiCalls.filtersTitle": "Filtros",
"admin.aiCalls.filtersHint": "Elige una empresa y acota por usuario, trabajo, rol o resultado.",
"admin.aiCalls.company": "Empresa",
"admin.aiCalls.allCompanies": "Todas las empresas",
"admin.aiCalls.role": "Rol",
"admin.aiCalls.outcome": "Resultado",
"admin.aiCalls.outcomeAll": "Todos",
"admin.aiCalls.outcomeOk": "Correctas",
"admin.aiCalls.outcomeFailed": "Fallidas",
"admin.aiCalls.user": "Usuario",
"admin.aiCalls.allUsers": "Todos los usuarios",
"admin.aiCalls.jobId": "ID de trabajo",
"admin.aiCalls.search": "Buscar",
"admin.aiCalls.searchHint": "Texto en el prompt o la respuesta",
"admin.aiCalls.rowCount": "{count} llamadas mostradas",
"admin.aiCalls.emptyTitle": "No hay llamadas de IA que coincidan",
"admin.aiCalls.emptyBody": "Las llamadas se guardan {days} dias. Ejecuta un trabajo y actualiza.",
"admin.aiCalls.colWhen": "Cuando",
"admin.aiCalls.colCompany": "Empresa",
"admin.aiCalls.colUser": "Usuario",
"admin.aiCalls.colRole": "Rol",
"admin.aiCalls.colOutcome": "Resultado",
"admin.aiCalls.colPrompt": "Prompt",
"admin.aiCalls.colTokens": "Tokens",
"admin.aiCalls.colTime": "Tiempo",
"admin.aiCalls.loadMore": "Cargar anteriores",
"admin.aiCalls.detailTitle": "Detalle de la llamada",
"admin.aiCalls.systemPrompt": "Prompt de sistema",
"admin.aiCalls.userPrompt": "Prompt de usuario",
"admin.aiCalls.response": "Respuesta",
"admin.aiCalls.loadFailed": "No se pudieron cargar las llamadas de IA.",
"admin.aiCalls.copyFailed": "No se pudo copiar al portapapeles.",
"common.copy": "Copiar",
};
+37 -1
View File
@@ -5714,5 +5714,41 @@ export const fr: MessageDict = {
"settings.permissions.role.viewer.desc": "Consulter le tableau de bord, le catalogue et les flux. Rien dautre.",
"settings.permissions.role.custom": "Personnalisé",
"settings.permissions.role.custom.desc": "Vous avez choisi les sections à la main : cela ne correspond à aucun rôle standard.",
"settings.permissions.advanced": "Ajuster des sections précises"
"settings.permissions.advanced": "Ajuster des sections précises",
"admin.nav.aiCalls": "Appels IA",
"admin.aiCalls.title": "Appels IA",
"admin.aiCalls.description": "Le prompt et la reponse exacts de chaque appel LLM. Conserves {days} jours.",
"admin.aiCalls.filtersTitle": "Filtres",
"admin.aiCalls.filtersHint": "Choisissez une entreprise, puis affinez par utilisateur, tache, role ou resultat.",
"admin.aiCalls.company": "Entreprise",
"admin.aiCalls.allCompanies": "Toutes les entreprises",
"admin.aiCalls.role": "Role",
"admin.aiCalls.outcome": "Resultat",
"admin.aiCalls.outcomeAll": "Tous",
"admin.aiCalls.outcomeOk": "Reussis",
"admin.aiCalls.outcomeFailed": "Echoues",
"admin.aiCalls.user": "Utilisateur",
"admin.aiCalls.allUsers": "Tous les utilisateurs",
"admin.aiCalls.jobId": "ID de tache",
"admin.aiCalls.search": "Rechercher",
"admin.aiCalls.searchHint": "Texte dans le prompt ou la reponse",
"admin.aiCalls.rowCount": "{count} appels affiches",
"admin.aiCalls.emptyTitle": "Aucun appel IA correspondant",
"admin.aiCalls.emptyBody": "Les appels sont conserves {days} jours. Lancez une tache puis actualisez.",
"admin.aiCalls.colWhen": "Quand",
"admin.aiCalls.colCompany": "Entreprise",
"admin.aiCalls.colUser": "Utilisateur",
"admin.aiCalls.colRole": "Role",
"admin.aiCalls.colOutcome": "Resultat",
"admin.aiCalls.colPrompt": "Prompt",
"admin.aiCalls.colTokens": "Jetons",
"admin.aiCalls.colTime": "Duree",
"admin.aiCalls.loadMore": "Charger plus anciens",
"admin.aiCalls.detailTitle": "Detail de l appel",
"admin.aiCalls.systemPrompt": "Prompt systeme",
"admin.aiCalls.userPrompt": "Prompt utilisateur",
"admin.aiCalls.response": "Reponse",
"admin.aiCalls.loadFailed": "Impossible de charger les appels IA.",
"admin.aiCalls.copyFailed": "Copie dans le presse-papiers impossible.",
"common.copy": "Copier",
};
+37 -1
View File
@@ -5714,5 +5714,41 @@ export const it: MessageDict = {
"settings.permissions.role.viewer.desc": "Consultare dashboard, catalogo e feed. Nientaltro.",
"settings.permissions.role.custom": "Personalizzato",
"settings.permissions.role.custom.desc": "Hai scelto le sezioni a mano, quindi non corrisponde a nessun ruolo standard.",
"settings.permissions.advanced": "Regola le singole sezioni"
"settings.permissions.advanced": "Regola le singole sezioni",
"admin.nav.aiCalls": "Chiamate IA",
"admin.aiCalls.title": "Chiamate IA",
"admin.aiCalls.description": "Il prompt e la risposta esatti di ogni chiamata LLM. Conservati {days} giorni.",
"admin.aiCalls.filtersTitle": "Filtri",
"admin.aiCalls.filtersHint": "Scegli un azienda, poi restringi per utente, processo, ruolo o esito.",
"admin.aiCalls.company": "Azienda",
"admin.aiCalls.allCompanies": "Tutte le aziende",
"admin.aiCalls.role": "Ruolo",
"admin.aiCalls.outcome": "Esito",
"admin.aiCalls.outcomeAll": "Tutti",
"admin.aiCalls.outcomeOk": "Riuscite",
"admin.aiCalls.outcomeFailed": "Fallite",
"admin.aiCalls.user": "Utente",
"admin.aiCalls.allUsers": "Tutti gli utenti",
"admin.aiCalls.jobId": "ID processo",
"admin.aiCalls.search": "Cerca",
"admin.aiCalls.searchHint": "Testo nel prompt o nella risposta",
"admin.aiCalls.rowCount": "{count} chiamate mostrate",
"admin.aiCalls.emptyTitle": "Nessuna chiamata IA corrispondente",
"admin.aiCalls.emptyBody": "Le chiamate si conservano {days} giorni. Avvia un processo e aggiorna.",
"admin.aiCalls.colWhen": "Quando",
"admin.aiCalls.colCompany": "Azienda",
"admin.aiCalls.colUser": "Utente",
"admin.aiCalls.colRole": "Ruolo",
"admin.aiCalls.colOutcome": "Esito",
"admin.aiCalls.colPrompt": "Prompt",
"admin.aiCalls.colTokens": "Token",
"admin.aiCalls.colTime": "Tempo",
"admin.aiCalls.loadMore": "Carica precedenti",
"admin.aiCalls.detailTitle": "Dettaglio chiamata",
"admin.aiCalls.systemPrompt": "Prompt di sistema",
"admin.aiCalls.userPrompt": "Prompt utente",
"admin.aiCalls.response": "Risposta",
"admin.aiCalls.loadFailed": "Impossibile caricare le chiamate IA.",
"admin.aiCalls.copyFailed": "Impossibile copiare negli appunti.",
"common.copy": "Copia",
};
+37 -1
View File
@@ -5714,5 +5714,41 @@ export const ja: MessageDict = {
"settings.permissions.role.viewer.desc": "ダッシュボード、カタログ、フィードの閲覧のみ。",
"settings.permissions.role.custom": "カスタム",
"settings.permissions.role.custom.desc": "エリアを個別に選択したため、標準ロールには一致しません。",
"settings.permissions.advanced": "エリアごとに細かく調整"
"settings.permissions.advanced": "エリアごとに細かく調整",
"admin.nav.aiCalls": "AI 呼び出し",
"admin.aiCalls.title": "AI 呼び出し",
"admin.aiCalls.description": "各 LLM 呼び出しの正確なプロンプトと応答。{days} 日間保持されます。",
"admin.aiCalls.filtersTitle": "フィルター",
"admin.aiCalls.filtersHint": "会社を選び、ユーザー・ジョブ・ロール・結果で絞り込みます。",
"admin.aiCalls.company": "会社",
"admin.aiCalls.allCompanies": "すべての会社",
"admin.aiCalls.role": "ロール",
"admin.aiCalls.outcome": "結果",
"admin.aiCalls.outcomeAll": "すべて",
"admin.aiCalls.outcomeOk": "成功",
"admin.aiCalls.outcomeFailed": "失敗",
"admin.aiCalls.user": "ユーザー",
"admin.aiCalls.allUsers": "すべてのユーザー",
"admin.aiCalls.jobId": "ジョブ ID",
"admin.aiCalls.search": "検索",
"admin.aiCalls.searchHint": "プロンプトまたは応答内のテキスト",
"admin.aiCalls.rowCount": "{count} 件の呼び出しを表示",
"admin.aiCalls.emptyTitle": "該当する AI 呼び出しはありません",
"admin.aiCalls.emptyBody": "呼び出しは {days} 日間保持されます。ジョブを実行して更新してください。",
"admin.aiCalls.colWhen": "日時",
"admin.aiCalls.colCompany": "会社",
"admin.aiCalls.colUser": "ユーザー",
"admin.aiCalls.colRole": "ロール",
"admin.aiCalls.colOutcome": "結果",
"admin.aiCalls.colPrompt": "プロンプト",
"admin.aiCalls.colTokens": "トークン",
"admin.aiCalls.colTime": "時間",
"admin.aiCalls.loadMore": "以前のものを読み込む",
"admin.aiCalls.detailTitle": "呼び出しの詳細",
"admin.aiCalls.systemPrompt": "システムプロンプト",
"admin.aiCalls.userPrompt": "ユーザープロンプト",
"admin.aiCalls.response": "応答",
"admin.aiCalls.loadFailed": "AI 呼び出しを読み込めませんでした。",
"admin.aiCalls.copyFailed": "クリップボードにコピーできませんでした。",
"common.copy": "コピー",
};
+37 -1
View File
@@ -5714,5 +5714,41 @@ export const nl: MessageDict = {
"settings.permissions.role.viewer.desc": "Dashboard, catalogus en feeds bekijken. Verder niets.",
"settings.permissions.role.custom": "Aangepast",
"settings.permissions.role.custom.desc": "Je hebt onderdelen handmatig gekozen, dus dit komt met geen enkele standaardrol overeen.",
"settings.permissions.advanced": "Losse onderdelen bijstellen"
"settings.permissions.advanced": "Losse onderdelen bijstellen",
"admin.nav.aiCalls": "AI-aanroepen",
"admin.aiCalls.title": "AI-aanroepen",
"admin.aiCalls.description": "De exacte prompt en het antwoord van elke LLM-aanroep. {days} dagen bewaard.",
"admin.aiCalls.filtersTitle": "Filters",
"admin.aiCalls.filtersHint": "Kies een bedrijf en verfijn op gebruiker, taak, rol of resultaat.",
"admin.aiCalls.company": "Bedrijf",
"admin.aiCalls.allCompanies": "Alle bedrijven",
"admin.aiCalls.role": "Rol",
"admin.aiCalls.outcome": "Resultaat",
"admin.aiCalls.outcomeAll": "Alle",
"admin.aiCalls.outcomeOk": "Geslaagd",
"admin.aiCalls.outcomeFailed": "Mislukt",
"admin.aiCalls.user": "Gebruiker",
"admin.aiCalls.allUsers": "Alle gebruikers",
"admin.aiCalls.jobId": "Taak-ID",
"admin.aiCalls.search": "Zoeken",
"admin.aiCalls.searchHint": "Tekst in prompt of antwoord",
"admin.aiCalls.rowCount": "{count} aanroepen getoond",
"admin.aiCalls.emptyTitle": "Geen overeenkomende AI-aanroepen",
"admin.aiCalls.emptyBody": "Aanroepen worden {days} dagen bewaard. Start een taak en ververs.",
"admin.aiCalls.colWhen": "Wanneer",
"admin.aiCalls.colCompany": "Bedrijf",
"admin.aiCalls.colUser": "Gebruiker",
"admin.aiCalls.colRole": "Rol",
"admin.aiCalls.colOutcome": "Resultaat",
"admin.aiCalls.colPrompt": "Prompt",
"admin.aiCalls.colTokens": "Tokens",
"admin.aiCalls.colTime": "Tijd",
"admin.aiCalls.loadMore": "Oudere laden",
"admin.aiCalls.detailTitle": "Aanroepdetails",
"admin.aiCalls.systemPrompt": "Systeemprompt",
"admin.aiCalls.userPrompt": "Gebruikersprompt",
"admin.aiCalls.response": "Antwoord",
"admin.aiCalls.loadFailed": "AI-aanroepen konden niet worden geladen.",
"admin.aiCalls.copyFailed": "Kopieren naar klembord mislukt.",
"common.copy": "Kopieren",
};
+37 -1
View File
@@ -5714,5 +5714,41 @@ export const pl: MessageDict = {
"settings.permissions.role.viewer.desc": "Przeglądanie pulpitu, katalogu i feedów. Nic więcej.",
"settings.permissions.role.custom": "Własna",
"settings.permissions.role.custom.desc": "Sekcje wybrano ręcznie, więc nie odpowiada to żadnej standardowej roli.",
"settings.permissions.advanced": "Dostosuj pojedyncze sekcje"
"settings.permissions.advanced": "Dostosuj pojedyncze sekcje",
"admin.nav.aiCalls": "Wywolania AI",
"admin.aiCalls.title": "Wywolania AI",
"admin.aiCalls.description": "Dokladny prompt i odpowiedz kazdego wywolania LLM. Przechowywane {days} dni.",
"admin.aiCalls.filtersTitle": "Filtry",
"admin.aiCalls.filtersHint": "Wybierz firme, potem zawez po uzytkowniku, zadaniu, roli lub wyniku.",
"admin.aiCalls.company": "Firma",
"admin.aiCalls.allCompanies": "Wszystkie firmy",
"admin.aiCalls.role": "Rola",
"admin.aiCalls.outcome": "Wynik",
"admin.aiCalls.outcomeAll": "Wszystkie",
"admin.aiCalls.outcomeOk": "Udane",
"admin.aiCalls.outcomeFailed": "Nieudane",
"admin.aiCalls.user": "Uzytkownik",
"admin.aiCalls.allUsers": "Wszyscy uzytkownicy",
"admin.aiCalls.jobId": "ID zadania",
"admin.aiCalls.search": "Szukaj",
"admin.aiCalls.searchHint": "Tekst w promptcie lub odpowiedzi",
"admin.aiCalls.rowCount": "Pokazano wywolan: {count}",
"admin.aiCalls.emptyTitle": "Brak pasujacych wywolan AI",
"admin.aiCalls.emptyBody": "Wywolania sa przechowywane {days} dni. Uruchom zadanie i odswiez.",
"admin.aiCalls.colWhen": "Kiedy",
"admin.aiCalls.colCompany": "Firma",
"admin.aiCalls.colUser": "Uzytkownik",
"admin.aiCalls.colRole": "Rola",
"admin.aiCalls.colOutcome": "Wynik",
"admin.aiCalls.colPrompt": "Prompt",
"admin.aiCalls.colTokens": "Tokeny",
"admin.aiCalls.colTime": "Czas",
"admin.aiCalls.loadMore": "Wczytaj starsze",
"admin.aiCalls.detailTitle": "Szczegoly wywolania",
"admin.aiCalls.systemPrompt": "Prompt systemowy",
"admin.aiCalls.userPrompt": "Prompt uzytkownika",
"admin.aiCalls.response": "Odpowiedz",
"admin.aiCalls.loadFailed": "Nie udalo sie wczytac wywolan AI.",
"admin.aiCalls.copyFailed": "Nie udalo sie skopiowac do schowka.",
"common.copy": "Kopiuj",
};
+37 -1
View File
@@ -5714,5 +5714,41 @@ export const pt: MessageDict = {
"settings.permissions.role.viewer.desc": "Ver o painel, o catálogo e os feeds. Mais nada.",
"settings.permissions.role.custom": "Personalizado",
"settings.permissions.role.custom.desc": "Escolheu as áreas manualmente, por isso não corresponde a nenhuma função padrão.",
"settings.permissions.advanced": "Ajustar áreas individuais"
"settings.permissions.advanced": "Ajustar áreas individuais",
"admin.nav.aiCalls": "Chamadas de IA",
"admin.aiCalls.title": "Chamadas de IA",
"admin.aiCalls.description": "O prompt e a resposta exatos de cada chamada ao LLM. Guardados {days} dias.",
"admin.aiCalls.filtersTitle": "Filtros",
"admin.aiCalls.filtersHint": "Escolha uma empresa e restrinja por utilizador, tarefa, funcao ou resultado.",
"admin.aiCalls.company": "Empresa",
"admin.aiCalls.allCompanies": "Todas as empresas",
"admin.aiCalls.role": "Funcao",
"admin.aiCalls.outcome": "Resultado",
"admin.aiCalls.outcomeAll": "Todos",
"admin.aiCalls.outcomeOk": "Bem-sucedidas",
"admin.aiCalls.outcomeFailed": "Falhadas",
"admin.aiCalls.user": "Utilizador",
"admin.aiCalls.allUsers": "Todos os utilizadores",
"admin.aiCalls.jobId": "ID da tarefa",
"admin.aiCalls.search": "Pesquisar",
"admin.aiCalls.searchHint": "Texto no prompt ou na resposta",
"admin.aiCalls.rowCount": "{count} chamadas mostradas",
"admin.aiCalls.emptyTitle": "Nenhuma chamada de IA corresponde",
"admin.aiCalls.emptyBody": "As chamadas sao guardadas {days} dias. Execute uma tarefa e atualize.",
"admin.aiCalls.colWhen": "Quando",
"admin.aiCalls.colCompany": "Empresa",
"admin.aiCalls.colUser": "Utilizador",
"admin.aiCalls.colRole": "Funcao",
"admin.aiCalls.colOutcome": "Resultado",
"admin.aiCalls.colPrompt": "Prompt",
"admin.aiCalls.colTokens": "Tokens",
"admin.aiCalls.colTime": "Tempo",
"admin.aiCalls.loadMore": "Carregar anteriores",
"admin.aiCalls.detailTitle": "Detalhe da chamada",
"admin.aiCalls.systemPrompt": "Prompt de sistema",
"admin.aiCalls.userPrompt": "Prompt do utilizador",
"admin.aiCalls.response": "Resposta",
"admin.aiCalls.loadFailed": "Nao foi possivel carregar as chamadas de IA.",
"admin.aiCalls.copyFailed": "Nao foi possivel copiar.",
"common.copy": "Copiar",
};
+36
View File
@@ -26,4 +26,40 @@ export const sl: MessageDict = {
"flash.admin.catalogCloned": "Copied {source} into {dest}: {products} products, {categories} taxonomy categories, {mapped_with} with mapped category.",
"flash.admin.syncA1Success": "Synced A1 for {name}: dump_mapped {dump_mapped} ({dump_status}), prompts {prompts}, hashes {hashes}, processed←mapped {categories}, mapped←processed {mapped_backfilled}. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.syncA1Error": "Sinhronizacija A1 za {name} ni uspela.",
"admin.nav.aiCalls": "Klici AI",
"admin.aiCalls.title": "Klici AI",
"admin.aiCalls.description": "Natancen poziv in odgovor vsakega klica LLM. Hranjeno {days} dni.",
"admin.aiCalls.filtersTitle": "Filtri",
"admin.aiCalls.filtersHint": "Izberite podjetje, nato zozite po uporabniku, opravilu, vlogi ali izidu.",
"admin.aiCalls.company": "Podjetje",
"admin.aiCalls.allCompanies": "Vsa podjetja",
"admin.aiCalls.role": "Vloga",
"admin.aiCalls.outcome": "Izid",
"admin.aiCalls.outcomeAll": "Vse",
"admin.aiCalls.outcomeOk": "Uspesno",
"admin.aiCalls.outcomeFailed": "Neuspesno",
"admin.aiCalls.user": "Uporabnik",
"admin.aiCalls.allUsers": "Vsi uporabniki",
"admin.aiCalls.jobId": "ID opravila",
"admin.aiCalls.search": "Iskanje",
"admin.aiCalls.searchHint": "Besedilo v pozivu ali odgovoru",
"admin.aiCalls.rowCount": "Prikazanih klicev: {count}",
"admin.aiCalls.emptyTitle": "Ni ujemajocih klicev AI",
"admin.aiCalls.emptyBody": "Zajeti klici se hranijo {days} dni. Zazenite opravilo in osvezite.",
"admin.aiCalls.colWhen": "Kdaj",
"admin.aiCalls.colCompany": "Podjetje",
"admin.aiCalls.colUser": "Uporabnik",
"admin.aiCalls.colRole": "Vloga",
"admin.aiCalls.colOutcome": "Izid",
"admin.aiCalls.colPrompt": "Poziv",
"admin.aiCalls.colTokens": "Zetoni",
"admin.aiCalls.colTime": "Cas",
"admin.aiCalls.loadMore": "Nalozi starejse",
"admin.aiCalls.detailTitle": "Podrobnosti klica",
"admin.aiCalls.systemPrompt": "Sistemski poziv",
"admin.aiCalls.userPrompt": "Uporabniski poziv",
"admin.aiCalls.response": "Odgovor",
"admin.aiCalls.loadFailed": "Klicev AI ni bilo mogoce naloziti.",
"admin.aiCalls.copyFailed": "Kopiranje v odlozisce ni uspelo.",
"common.copy": "Kopiraj",
};
@@ -0,0 +1,408 @@
<script lang="ts">
import { i18n } from "$lib/i18n";
import { onMount } from "svelte";
import { api, failureMessage } from "$lib/api";
import { requirePlatformAdmin } from "$lib/admin-gate";
import { formatDate } from "$lib/utils";
import PageShell from "$lib/components/PageShell.svelte";
import Alert from "$lib/components/Alert.svelte";
import ForbiddenEmptyState from "$lib/components/ForbiddenEmptyState.svelte";
import Spinner from "$lib/components/Spinner.svelte";
import EmptyState from "$lib/components/EmptyState.svelte";
import {
Badge,
Button,
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
TableShell,
type BadgeVariant
} from "$lib/components/ui";
import { RefreshCw, Copy, X } from "@lucide/svelte";
type CallRow = {
id: string;
company_id?: string;
company_name?: string;
user_id?: string;
user_email?: string;
job_id?: string;
raw_product_id?: string;
role?: string;
provider_mode?: string;
model?: string;
outcome?: string;
error?: string;
finish_reason?: string;
prompt_tokens?: number;
completion_tokens?: number;
total_tokens?: number;
duration_ms?: number;
created_at?: string;
system_length?: number;
user_length?: number;
response_length?: number;
user_preview?: string;
response_preview?: string;
};
type CallDetail = CallRow & {
system_prompt?: string;
user_prompt?: string;
response_text?: string;
retention_days?: number;
};
type Company = { id: string; name?: string };
let loading = $state(true);
let accessDenied = $state(false);
let refreshing = $state(false);
let error = $state("");
let copied = $state("");
let companies = $state<Company[]>([]);
let rows = $state<CallRow[]>([]);
let nextBefore = $state<string | null>(null);
let retentionDays = $state(7);
// Filters — company first, then narrow by who/what.
let companyId = $state("");
let role = $state("all");
let outcome = $state("all");
let userId = $state("");
let jobId = $state("");
let search = $state("");
let selected = $state<CallDetail | null>(null);
let detailLoading = $state(false);
const ROLES = ["all", "processing", "categorize", "seo_meta", "campaign", "support", "other"];
// Users seen in the current result set — enough to filter by person without a
// second round trip, and it only ever offers users who actually made calls.
const usersInView = $derived.by(() => {
const seen = new Map<string, string>();
for (const r of rows) {
if (r.user_id) seen.set(r.user_id, r.user_email || r.user_id);
}
return [...seen.entries()].map(([id, label]) => ({ id, label }));
});
function queryString(before?: string | null) {
const p = new URLSearchParams();
if (companyId) p.set("company_id", companyId);
if (role && role !== "all") p.set("role", role);
if (outcome && outcome !== "all") p.set("outcome", outcome);
if (userId) p.set("user_id", userId);
if (jobId.trim()) p.set("job_id", jobId.trim());
if (search.trim()) p.set("q", search.trim());
if (before) p.set("before", before);
p.set("limit", "50");
return p.toString();
}
async function load(append = false) {
error = "";
refreshing = true;
try {
const res = await api<{
items?: CallRow[];
next_before?: string;
retention_days?: number;
}>(`/api/admin/ai-calls?${queryString(append ? nextBefore : null)}`);
rows = append ? [...rows, ...(res.items ?? [])] : (res.items ?? []);
nextBefore = res.next_before ?? null;
if (res.retention_days) retentionDays = res.retention_days;
} catch (err) {
error = failureMessage(err, i18n.t("admin.aiCalls.loadFailed"));
} finally {
refreshing = false;
loading = false;
}
}
async function openDetail(id: string) {
detailLoading = true;
error = "";
try {
selected = await api<CallDetail>(`/api/admin/ai-calls/${id}`);
} catch (err) {
error = failureMessage(err, i18n.t("admin.aiCalls.loadFailed"));
selected = null;
} finally {
detailLoading = false;
}
}
async function copy(label: string, text: string) {
try {
await navigator.clipboard.writeText(text ?? "");
copied = label;
setTimeout(() => (copied = ""), 1500);
} catch {
error = i18n.t("admin.aiCalls.copyFailed");
}
}
function outcomeVariant(o?: string): BadgeVariant {
return o === "failed" ? "destructive" : "secondary";
}
function ms(n?: number) {
if (!n && n !== 0) return "—";
return n < 1000 ? `${n} ms` : `${(n / 1000).toFixed(1)} s`;
}
onMount(async () => {
if (!(await requirePlatformAdmin())) {
accessDenied = true;
loading = false;
return;
}
try {
const res = await api<{ items?: Company[] }>("/api/admin/companies?limit=200");
companies = res.items ?? [];
} catch {
// Company list is a convenience filter; the page still works without it.
}
await load();
});
</script>
<PageShell
title={i18n.t("admin.aiCalls.title")}
description={i18n.t("admin.aiCalls.description", { days: retentionDays })}
>
{#if accessDenied}
<ForbiddenEmptyState />
{:else if loading}
<Spinner />
{:else}
{#if error}
<Alert tone="error" message={error} />
{/if}
<Card>
<CardHeader>
<CardTitle>{i18n.t("admin.aiCalls.filtersTitle")}</CardTitle>
<CardDescription>{i18n.t("admin.aiCalls.filtersHint")}</CardDescription>
</CardHeader>
<CardContent>
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
<label class="text-sm">
<span class="mb-1 block text-muted-foreground">
{i18n.t("admin.aiCalls.company")}
</span>
<select
class="w-full rounded-md border bg-background p-2"
bind:value={companyId}
onchange={() => load()}
>
<option value="">{i18n.t("admin.aiCalls.allCompanies")}</option>
{#each companies as c (c.id)}
<option value={c.id}>{c.name || c.id}</option>
{/each}
</select>
</label>
<label class="text-sm">
<span class="mb-1 block text-muted-foreground">{i18n.t("admin.aiCalls.role")}</span>
<select
class="w-full rounded-md border bg-background p-2"
bind:value={role}
onchange={() => load()}
>
{#each ROLES as r (r)}
<option value={r}>{r}</option>
{/each}
</select>
</label>
<label class="text-sm">
<span class="mb-1 block text-muted-foreground">{i18n.t("admin.aiCalls.outcome")}</span>
<select
class="w-full rounded-md border bg-background p-2"
bind:value={outcome}
onchange={() => load()}
>
<option value="all">{i18n.t("admin.aiCalls.outcomeAll")}</option>
<option value="ok">{i18n.t("admin.aiCalls.outcomeOk")}</option>
<option value="failed">{i18n.t("admin.aiCalls.outcomeFailed")}</option>
</select>
</label>
<label class="text-sm">
<span class="mb-1 block text-muted-foreground">{i18n.t("admin.aiCalls.user")}</span>
<select
class="w-full rounded-md border bg-background p-2"
bind:value={userId}
onchange={() => load()}
>
<option value="">{i18n.t("admin.aiCalls.allUsers")}</option>
{#each usersInView as u (u.id)}
<option value={u.id}>{u.label}</option>
{/each}
</select>
</label>
<label class="text-sm">
<span class="mb-1 block text-muted-foreground">{i18n.t("admin.aiCalls.jobId")}</span>
<input
class="w-full rounded-md border bg-background p-2 font-mono text-xs"
placeholder="uuid"
bind:value={jobId}
onchange={() => load()}
/>
</label>
<label class="text-sm">
<span class="mb-1 block text-muted-foreground">{i18n.t("admin.aiCalls.search")}</span>
<input
class="w-full rounded-md border bg-background p-2"
placeholder={i18n.t("admin.aiCalls.searchHint")}
bind:value={search}
onkeydown={(e) => e.key === "Enter" && load()}
/>
</label>
</div>
<div class="mt-3 flex items-center gap-2">
<Button onclick={() => load()} disabled={refreshing}>
<RefreshCw class="mr-2 size-4" />
{i18n.t("common.refresh")}
</Button>
<span class="text-xs text-muted-foreground">
{i18n.t("admin.aiCalls.rowCount", { count: rows.length })}
</span>
</div>
</CardContent>
</Card>
{#if rows.length === 0}
<EmptyState
title={i18n.t("admin.aiCalls.emptyTitle")}
message={i18n.t("admin.aiCalls.emptyBody", { days: retentionDays })}
/>
{:else}
<TableShell>
<TableHeader>
<TableRow>
<TableHead>{i18n.t("admin.aiCalls.colWhen")}</TableHead>
<TableHead>{i18n.t("admin.aiCalls.colCompany")}</TableHead>
<TableHead>{i18n.t("admin.aiCalls.colUser")}</TableHead>
<TableHead>{i18n.t("admin.aiCalls.colRole")}</TableHead>
<TableHead>{i18n.t("admin.aiCalls.colOutcome")}</TableHead>
<TableHead>{i18n.t("admin.aiCalls.colPrompt")}</TableHead>
<TableHead class="text-right">{i18n.t("admin.aiCalls.colTokens")}</TableHead>
<TableHead class="text-right">{i18n.t("admin.aiCalls.colTime")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{#each rows as row (row.id)}
<TableRow
class="cursor-pointer"
onclick={() => openDetail(row.id)}
>
<TableCell class="whitespace-nowrap text-xs">{formatDate(row.created_at)}</TableCell>
<TableCell class="text-xs">{row.company_name || "—"}</TableCell>
<TableCell class="text-xs">{row.user_email || "—"}</TableCell>
<TableCell><Badge variant="outline">{row.role}</Badge></TableCell>
<TableCell>
<Badge variant={outcomeVariant(row.outcome)}>{row.outcome}</Badge>
</TableCell>
<TableCell class="max-w-md truncate text-xs text-muted-foreground">
{row.user_preview}
</TableCell>
<TableCell class="text-right text-xs">{row.total_tokens ?? 0}</TableCell>
<TableCell class="text-right text-xs">{ms(row.duration_ms)}</TableCell>
</TableRow>
{/each}
</TableBody>
</TableShell>
{#if nextBefore}
<div class="mt-3">
<Button variant="outline" onclick={() => load(true)} disabled={refreshing}>
{i18n.t("admin.aiCalls.loadMore")}
</Button>
</div>
{/if}
{/if}
{#if detailLoading}
<Spinner />
{/if}
{#if selected}
<Card class="mt-4">
<CardHeader>
<div class="flex items-start justify-between gap-2">
<div>
<CardTitle>{i18n.t("admin.aiCalls.detailTitle")}</CardTitle>
<CardDescription>
{selected.company_name || "—"} · {selected.user_email || "—"} · {selected.role} ·
{selected.model || "—"} ({selected.provider_mode || "—"})
</CardDescription>
</div>
<Button variant="ghost" size="sm" onclick={() => (selected = null)}>
<X class="size-4" />
</Button>
</div>
</CardHeader>
<CardContent class="space-y-4">
<dl class="grid gap-2 text-xs sm:grid-cols-2 lg:grid-cols-4">
<div>
<dt class="text-muted-foreground">{i18n.t("admin.aiCalls.colWhen")}</dt>
<dd>{formatDate(selected.created_at)}</dd>
</div>
<div>
<dt class="text-muted-foreground">{i18n.t("admin.aiCalls.colTokens")}</dt>
<dd>
{selected.prompt_tokens ?? 0} in / {selected.completion_tokens ?? 0} out
</dd>
</div>
<div>
<dt class="text-muted-foreground">{i18n.t("admin.aiCalls.colTime")}</dt>
<dd>{ms(selected.duration_ms)} · {selected.finish_reason || "—"}</dd>
</div>
<div>
<dt class="text-muted-foreground">{i18n.t("admin.aiCalls.jobId")}</dt>
<dd class="truncate font-mono">{selected.job_id || "—"}</dd>
</div>
</dl>
{#if selected.error}
<Alert tone="error" message={selected.error} />
{/if}
{#each [{ key: "system", label: i18n.t("admin.aiCalls.systemPrompt"), body: selected.system_prompt }, { key: "user", label: i18n.t("admin.aiCalls.userPrompt"), body: selected.user_prompt }, { key: "response", label: i18n.t("admin.aiCalls.response"), body: selected.response_text }] as part (part.key)}
<div>
<div class="mb-1 flex items-center justify-between">
<span class="text-sm font-medium">{part.label}</span>
<Button
variant="ghost"
size="sm"
onclick={() => copy(part.key, part.body ?? "")}
>
<Copy class="mr-1 size-3" />
{copied === part.key ? i18n.t("common.copied") : i18n.t("common.copy")}
</Button>
</div>
<pre
class="max-h-96 overflow-auto rounded-md border bg-muted p-3 text-xs whitespace-pre-wrap break-words">{part.body ||
"—"}</pre>
</div>
{/each}
</CardContent>
</Card>
{/if}
{/if}
</PageShell>