update see calss
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -5714,5 +5714,41 @@ export const fr: MessageDict = {
|
||||
"settings.permissions.role.viewer.desc": "Consulter le tableau de bord, le catalogue et les flux. Rien d’autre.",
|
||||
"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",
|
||||
};
|
||||
|
||||
@@ -5714,5 +5714,41 @@ export const it: MessageDict = {
|
||||
"settings.permissions.role.viewer.desc": "Consultare dashboard, catalogo e feed. Nient’altro.",
|
||||
"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",
|
||||
};
|
||||
|
||||
@@ -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": "コピー",
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user