File size: 9,276 Bytes
30cd0c9
98ca5fc
 
30cd0c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98ca5fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30cd0c9
 
98ca5fc
30cd0c9
 
 
 
 
 
98ca5fc
30cd0c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98ca5fc
 
30cd0c9
98ca5fc
 
30cd0c9
 
98ca5fc
 
30cd0c9
 
 
 
 
98ca5fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30cd0c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
import { useState } from "react";
import { ChevronDown, ChevronRight, Database, Loader2, Route, Wrench } from "lucide-react";
import type { Observability, ObservabilityDataUsed } from "@/services/agenticApi";
import { formatDateTime } from "./utils";

interface MessageTraceabilityProps {
  observability?: Observability | null;
  loading?: boolean;
  error?: string | null;
  sources?: unknown[];
}

function renderUnknown(value: unknown) {
  if (value == null) return "-";
  if (typeof value === "string") return value;
  try {
    return JSON.stringify(value, null, 2);
  } catch {
    return String(value);
  }
}

function sourceLabel(source: unknown, index: number) {
  if (source && typeof source === "object") {
    const record = source as Record<string, unknown>;
    return String(record.name ?? record.title ?? record.source ?? record.source_id ?? record.id ?? `Source ${index + 1}`);
  }
  return String(source ?? `Source ${index + 1}`);
}

function DataUsedCard({ item, index }: { item: ObservabilityDataUsed; index: number }) {
  return (
    <div className="rounded-md border border-slate-200 bg-white p-2">
      <div className="flex items-center justify-between gap-2">
        <p className="font-medium text-slate-900">{item.source?.name ?? `Data source ${index + 1}`}</p>
        {item.source?.type && <span className="rounded bg-slate-100 px-1.5 py-0.5 text-[10px] uppercase text-slate-500">{item.source.type}</span>}
      </div>

      {item.tables && item.tables.length > 0 && (
        <p className="mt-1.5 text-slate-600">
          Tables: {item.tables.map((table) => table.name).join(", ")}
        </p>
      )}

      {item.columns_read && item.columns_read.length > 0 && (
        <div className="mt-1.5 flex flex-wrap gap-1">
          {item.columns_read.map((column) => (
            <span key={column.id} className="rounded border border-slate-200 bg-slate-50 px-1.5 py-0.5 text-[11px] text-slate-600">
              {column.table ? `${column.table}.${column.name}` : column.name}
            </span>
          ))}
        </div>
      )}

      {item.group_by && item.group_by.length > 0 && (
        <p className="mt-1.5 text-slate-600">Grouped by: {item.group_by.join(", ")}</p>
      )}

      {typeof item.rows_returned === "number" && (
        <p className="mt-1.5 text-slate-600">Rows returned: {item.rows_returned}</p>
      )}

      {item.query && (
        <pre className="mt-2 max-h-40 overflow-auto rounded bg-slate-950 p-2 text-[11px] leading-4 text-slate-100">{item.query}</pre>
      )}
    </div>
  );
}

export function MessageTraceability({ observability, loading, error, sources = [] }: MessageTraceabilityProps) {
  const [open, setOpen] = useState(false);
  const [detailsOpen, setDetailsOpen] = useState(false);
  const traceSources = observability?.sources?.length ? observability.sources : sources;
  const hasTrace = Boolean(observability || loading || error || traceSources.length > 0);

  if (!hasTrace) return null;

  const toolCalls = observability?.tool_calls ?? [];
  const dataUsed = observability?.data_used ?? [];

  return (
    <div className="mt-3 border-t border-slate-200 pt-3">
      <button
        type="button"
        title="Show traceability for this AI answer"
        aria-expanded={open}
        onClick={() => setOpen((current) => !current)}
        className="inline-flex items-center gap-2 rounded-md px-2 py-1 text-xs font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-950"
      >
        {open ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
        Traceability
        {loading && <Loader2 className="h-3.5 w-3.5 animate-spin" />}
      </button>

      {open && (
        <div className="mt-3 space-y-3 rounded-md border border-slate-200 bg-slate-50 p-3 text-xs text-slate-700">
          {loading && <p className="text-slate-500">Traceability is being prepared.</p>}
          {error && <p className="text-red-600">{error}</p>}

          {observability?.generated_at && (
            <p className="text-[11px] uppercase tracking-wide text-slate-400">Generated {formatDateTime(observability.generated_at)}</p>
          )}

          {observability?.intent && (
            <div>
              <p className="font-semibold text-slate-900">Intent</p>
              <p className="mt-1 leading-5">{observability.intent}</p>
            </div>
          )}

          {(observability?.planning || observability?.thinking) && (
            <div className="space-y-2">
              <div className="flex items-center gap-2 font-semibold text-slate-900">
                <Route className="h-3.5 w-3.5" />
                Thought and planning
              </div>
              {observability.thinking && <p className="leading-5">{observability.thinking}</p>}
              {observability.planning?.goal_restated && <p className="leading-5">{observability.planning.goal_restated}</p>}
              {observability.planning?.assumptions && observability.planning.assumptions.length > 0 && (
                <ul className="list-disc space-y-1 pl-4">
                  {observability.planning.assumptions.map((item) => <li key={item}>{item}</li>)}
                </ul>
              )}
              {observability.planning?.steps && observability.planning.steps.length > 0 && (
                <div className="space-y-1">
                  {observability.planning.steps.map((step, index) => (
                    <div key={`${step.stage ?? "step"}-${index}`} className="rounded border border-slate-200 bg-white px-2 py-1.5">
                      <p className="font-medium text-slate-800">{step.stage ?? `Step ${step.step ?? index + 1}`}</p>
                      {step.objective && <p className="mt-0.5 text-slate-600">{step.objective}</p>}
                    </div>
                  ))}
                </div>
              )}
            </div>
          )}

          <div>
            <div className="flex items-center gap-2 font-semibold text-slate-900">
              <Database className="h-3.5 w-3.5" />
              Data used
            </div>
            {dataUsed.length === 0 ? (
              <p className="mt-1 text-slate-500">No data usage recorded for this answer.</p>
            ) : (
              <div className="mt-2 space-y-2">
                {dataUsed.map((item, index) => (
                  <DataUsedCard key={`${item.source?.id ?? "source"}-${index}`} item={item} index={index} />
                ))}
              </div>
            )}
          </div>

          <div>
            <button
              type="button"
              title="Show tool call details"
              aria-expanded={detailsOpen}
              onClick={() => setDetailsOpen((current) => !current)}
              className="inline-flex items-center gap-2 rounded-md px-1.5 py-1 text-xs font-semibold text-slate-900 hover:bg-slate-100"
            >
              {detailsOpen ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
              <Wrench className="h-3.5 w-3.5" />
              Details
            </button>

            {detailsOpen && (
              toolCalls.length === 0 ? (
                <p className="mt-1 pl-1.5 text-slate-500">No tool call recorded for this answer.</p>
              ) : (
                <div className="mt-2 space-y-2 pl-1.5">
                  {toolCalls.map((tool, index) => (
                    <div key={`${tool.name}-${index}`} className="rounded-md border border-slate-200 bg-white p-2">
                      <div className="flex items-center justify-between gap-2">
                        <p className="font-medium text-slate-900">{tool.name}</p>
                        {tool.status && <span className="rounded bg-slate-100 px-1.5 py-0.5 text-[10px] uppercase text-slate-500">{tool.status}</span>}
                      </div>
                      {tool.summary && <p className="mt-1 text-slate-600">{tool.summary}</p>}
                      {tool.input !== undefined && (
                        <pre className="mt-2 max-h-40 overflow-auto rounded bg-slate-950 p-2 text-[11px] leading-4 text-slate-100">{renderUnknown(tool.input)}</pre>
                      )}
                      {tool.output !== undefined && (
                        <pre className="mt-2 max-h-40 overflow-auto rounded bg-white p-2 text-[11px] leading-4 text-slate-600 ring-1 ring-slate-200">{renderUnknown(tool.output)}</pre>
                      )}
                      {tool.error && <p className="mt-2 text-red-600">{tool.error}</p>}
                    </div>
                  ))}
                </div>
              )
            )}
          </div>

          {traceSources.length > 0 && (
            <div>
              <p className="font-semibold text-slate-900">Citations and sources</p>
              <div className="mt-2 flex flex-wrap gap-1.5">
                {traceSources.map((source, index) => (
                  <span key={`${sourceLabel(source, index)}-${index}`} className="rounded-md border border-slate-200 bg-white px-2 py-1 text-[11px] text-slate-600">
                    {sourceLabel(source, index)}
                  </span>
                ))}
              </div>
            </div>
          )}
        </div>
      )}
    </div>
  );
}