Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions internal/admin/audit_projection.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,10 @@ func looksLikeResponsesOutput(v any) bool {
// slimConversationEntry strips the fields the Interactions drawer never
// reads from a conversation-thread entry. Request headers are retained because
// the drawer uses the safe, redacted subset to continue the same session from
// the original endpoint. Response headers, attempts, and request revisions
// only inflate the response.
// the original endpoint. Response headers and attempts only inflate the
// response. Request revisions keep their metadata — the drawer's request-step
// picker needs to know which rewrites ran — but drop the heavy per-revision
// bodies and details; the drawer lazy-loads those from /admin/audit/detail.
func slimConversationEntry(entry *auditlog.LogEntry) {
d := entry.Data
if d == nil {
Expand All @@ -138,7 +140,15 @@ func slimConversationEntry(entry *auditlog.LogEntry) {
}
slim := *d
slim.Attempts = nil
slim.RequestRevisions = nil
slim.ResponseHeaders = nil
if len(slim.RequestRevisions) > 0 {
revisions := make([]auditlog.RequestRevisionSnapshot, len(slim.RequestRevisions))
copy(revisions, slim.RequestRevisions)
for i := range revisions {
revisions[i].Body = nil
revisions[i].Detail = nil
}
slim.RequestRevisions = revisions
}
entry.Data = &slim
}
14 changes: 12 additions & 2 deletions internal/admin/audit_projection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,18 @@ func TestAuditConversationSlimsEntries(t *testing.T) {
if d.ErrorMessage != "boom" {
t.Error("error_message feeds the drawer's error rendering; it must survive")
}
if d.Attempts != nil || d.RequestRevisions != nil || d.ResponseHeaders != nil {
t.Errorf("attempts/revisions/response headers must be stripped from conversation entries, got %+v", d)
if d.Attempts != nil || d.ResponseHeaders != nil {
t.Errorf("attempts/response headers must be stripped from conversation entries, got %+v", d)
}
if len(d.RequestRevisions) != 1 {
t.Fatalf("revision metadata feeds the drawer's request-step picker; it must survive, got %+v", d.RequestRevisions)
}
rev := d.RequestRevisions[0]
if rev.Body != nil || rev.Detail != nil {
t.Errorf("revision bodies/details must be stripped from conversation entries, got %+v", rev)
}
if rev.Rewriter != "pro-token-compression" || rev.Seq != 1 || rev.BytesBefore != 2000 || rev.BytesAfter != 1000 {
t.Errorf("revision metadata must survive, got %+v", rev)
}
if d.RequestHeaders["content-type"] != "application/json" {
t.Errorf("redacted request headers are required for follow-ups, got %+v", d.RequestHeaders)
Expand Down
36 changes: 0 additions & 36 deletions internal/admin/dashboard/static/dist/assets/index-C_oa9zmJ.js

This file was deleted.

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions internal/admin/dashboard/static/dist/assets/index-nIAZ31DK.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions internal/admin/dashboard/static/dist/index.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions web/dashboard/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,11 @@
"interaction_request_timed_out": "The request timed out.",
"interaction_send_failed": "Failed to send message.",
"interaction_saved_unavailable": "Message sent, but its saved interaction is not available yet.",
"interaction_request_step": "Request step",
"interaction_request_step_original": "Original request",
"interaction_request_step_after": "After {rewriter}",
"interaction_request_step_final": "Sent to provider (after {rewriter})",
"interaction_request_step_help": "Preview the selected request at any rewrite step. By default the final shape — what was sent to the provider — is shown.",
"interaction_role_system": "System Prompt",
"interaction_role_agent": "Agent",
"interaction_role_error": "Error",
Expand Down
5 changes: 5 additions & 0 deletions web/dashboard/messages/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,11 @@
"interaction_request_timed_out": "Upłynął limit czasu Requestu.",
"interaction_send_failed": "Wysyłanie wiadomości nie powiodło się.",
"interaction_saved_unavailable": "Wiadomość została wysłana, ale jej zapisana interakcja nie jest jeszcze dostępna.",
"interaction_request_step": "Krok Requestu",
"interaction_request_step_original": "Oryginalny Request",
"interaction_request_step_after": "Po {rewriter}",
"interaction_request_step_final": "Wysłany do dostawcy (po {rewriter})",
"interaction_request_step_help": "Podgląd wybranego Requestu na dowolnym kroku przepisywania. Domyślnie pokazywany jest ostateczny kształt — to, co zostało wysłane do dostawcy.",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"interaction_role_system": "Prompt systemowy",
"interaction_role_agent": "Agent",
"interaction_role_error": "Błąd",
Expand Down
1 change: 1 addition & 0 deletions web/dashboard/src/pages/audit-logs/AuditPane.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
conversationDrawer.handleBodyConversationClick(
event,
pane.entry,
pane.requestStep,
)}>{@html renderedBody}</pre>
</div>
{/if}
Expand Down
115 changes: 88 additions & 27 deletions web/dashboard/src/pages/audit-logs/ConversationDrawer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import * as m from "$lib/paraglide/messages.js";
import ChatMessage from "./ChatMessage.svelte";
import { conversationDrawer } from "./conversationDrawer.svelte.js";
import { conversationRequestStepID } from "./conversation-helpers.js";
import {
DEFAULT_CONVERSATION_PANEL_WIDTH,
clampConversationPanelWidth,
Expand All @@ -36,6 +37,14 @@
let showPromptCache = $state(readStored(promptCacheFillStorageKey, "true") !== "false");
let resizePointerID = null;

function requestStepLabel(step) {
if (!step || step.seq === 0) return m.interaction_request_step_original();
if (step.isFinal) {
return m.interaction_request_step_final({ rewriter: step.rewriter });
}
return m.interaction_request_step_after({ rewriter: step.rewriter });
}

function interactionsTransition(node, { fullscreen: isFullscreen, revealWithoutSlide = false }) {
const duration = motionDuration(180);
if (isFullscreen) return fly(node, { x: panelWidth, duration, easing: cubicOut });
Expand Down Expand Up @@ -245,6 +254,7 @@
onkeydown={resizeWithKeyboard}
></div>
<div class="conversation-drawer-header">
<div class="conversation-drawer-header-row">
<div class="conversation-drawer-title">
<h3 id="interactions-drawer-title">{m.interaction_title()}</h3>
<span
Expand Down Expand Up @@ -285,6 +295,47 @@
bind:el={drawer.conversationCloseBtnEl}
/>
</div>
</div>
{#if drawer.conversationRequestSteps().length > 1}
<label
class="conversation-step-picker"
title={m.interaction_request_step_help()}
>
<span>{m.interaction_request_step()}</span>
<select
value={drawer.conversationRequestStep}
onchange={(event) => drawer.selectRequestStep(event.currentTarget.value)}
>
{#each drawer.conversationRequestSteps() as step (step.id)}
<option value={conversationRequestStepID(step)}>{requestStepLabel(step)}</option>
{/each}
</select>
</label>
{/if}
{#if drawer.conversationMessages.some((msg) =>
Number(msg.promptCacheRatio || 0) > 0
)}
<button
type="button"
class="conversation-cache-legend"
role="switch"
aria-checked={showPromptCache}
title={showPromptCache
? m.interaction_hide_cache_fill()
: m.interaction_show_cache_fill()}
onclick={togglePromptCacheFill}
>
<span class="conversation-cache-switch" class:is-active={showPromptCache} aria-hidden="true">
<span class="conversation-cache-switch-thumb"></span>
</span>
<span
>{m.interaction_cache_legend()}
<span class="conversation-cache-estimate"
>{m.interaction_estimated()}</span
></span
>
</button>
{/if}
</div>

<div id="interactions-drawer-content">
Expand Down Expand Up @@ -323,30 +374,6 @@
{/if}

{#if drawer.conversationMessages.length > 0}
{#if drawer.conversationMessages.some((msg) =>
Number(msg.promptCacheRatio || 0) > 0
)}
<button
type="button"
class="conversation-cache-legend"
role="switch"
aria-checked={showPromptCache}
title={showPromptCache
? m.interaction_hide_cache_fill()
: m.interaction_show_cache_fill()}
onclick={togglePromptCacheFill}
>
<span class="conversation-cache-switch" class:is-active={showPromptCache} aria-hidden="true">
<span class="conversation-cache-switch-thumb"></span>
</span>
<span
>{m.interaction_cache_legend()}
<span class="conversation-cache-estimate"
>{m.interaction_estimated()}</span
></span
>
</button>
{/if}
<div class="conversation-thread" bind:this={drawer.conversationThreadEl}>
{#each drawer.conversationMessages as msg (msg.uid)}
<ChatMessage {msg} {showPromptCache} />
Expand Down Expand Up @@ -469,12 +496,18 @@
}

.conversation-drawer-header {
display: flex;
flex-direction: column;
gap: 9px;
padding: 14px 16px;
border-bottom: 1px solid var(--border);
}

.conversation-drawer-header-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 14px 16px;
border-bottom: 1px solid var(--border);
}

.conversation-drawer-header-actions {
Expand Down Expand Up @@ -621,11 +654,39 @@
font-size: 12px;
}

.conversation-step-picker {
display: flex;
align-items: center;
gap: 7px;
color: var(--text-muted);
font-size: 11px;
}

.conversation-step-picker span {
flex: 0 0 auto;
}

.conversation-step-picker select {
min-width: 0;
max-width: 100%;
padding: 3px 6px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg);
color: var(--text);
font-size: 11px;
font-family: inherit;
}

.conversation-step-picker select:focus-visible {
outline: 2px solid color-mix(in srgb, var(--accent) 38%, transparent);
outline-offset: 1px;
}

.conversation-cache-legend {
display: flex;
align-items: center;
gap: 7px;
margin: 12px 16px 0;
padding: 0;
border: 0;
background: transparent;
Expand Down
6 changes: 6 additions & 0 deletions web/dashboard/src/pages/audit-logs/audit-logic.js
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,9 @@ export function auditRequestRevisionPane(entry, revision) {
title: m.audit_rewritten_title(),
direction: "request",
seq: single ? 0 : Number((revision && revision.seq) || 0),
// Opening the Interactions drawer from this pane previews this rewrite
// step (see conversationDrawer.openConversation).
requestStep: "revision-" + Number((revision && revision.seq) || 0),
kind: revision && revision.rewriter ? String(revision.rewriter) : "",
savingsLabel: auditRevisionPercentLabel(revision),
layout: "split",
Expand Down Expand Up @@ -838,6 +841,9 @@ export function auditRequestPane(entry, extractSegments) {
return {
title: m.audit_request_title(),
direction: "request",
// Opening the Interactions drawer from this pane previews the original
// client request (see conversationDrawer.openConversation).
requestStep: "original",
layout: "split",
entry,
copyHeaders: data && data.request_headers,
Expand Down
29 changes: 29 additions & 0 deletions web/dashboard/src/pages/audit-logs/audit-records.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export function mergeAuditRecord(previous, incoming) {
merged.data = current.data;
} else if (plainObject(current.data) && plainObject(patch.data)) {
merged.data = { ...current.data, ...patch.data };
const revisions = mergedRequestRevisions(
current.data.request_revisions,
patch.data.request_revisions,
);
if (revisions) merged.data.request_revisions = revisions;
}

// Lifecycle fields belong to the live transport. Persisted/list projections
Expand Down Expand Up @@ -63,6 +68,30 @@ export function isLiveAuditRecordChange(change) {
return liveAuditEvents.has(String(change && change.eventType || "").trim());
}

// Revision bodies arrive only via the detail endpoint; slim projections
// (conversation, list) carry metadata-only revisions. Merge per step so a
// later slim refresh cannot downgrade an already-loaded rewrite body.
function mergedRequestRevisions(currentRevisions, patchRevisions) {
if (!Array.isArray(currentRevisions) || !Array.isArray(patchRevisions)) {
return null;
}
return patchRevisions.map((revision) => {
const seq = Number(revision && revision.seq || 0);
const previous = currentRevisions.find(
(candidate) => Number(candidate && candidate.seq || 0) === seq,
);
if (!previous) return revision;
const richer = { ...revision };
if ((richer.body == null || richer.body === "") && previous.body != null) {
richer.body = previous.body;
}
if (richer.detail == null && previous.detail != null) {
richer.detail = previous.detail;
}
return richer;
});
}

function plainObject(value) {
return !!value && typeof value === "object" && !Array.isArray(value);
}
Loading