main
js 235 lines 7.25 KB
Raw
1 import { createStore } from "/js/AlpineStore.js";
2 import { createActionButton, copyToClipboard } from "/components/messages/action-buttons/simple-action-buttons.js";
3 import { store as notificationStore } from "/components/notifications/notification-store.js";
4 import { formatDateTime } from "/js/time-utils.js";
5
6 // Step Detail Store - manages the step detail modal
7
8 const model = {
9 // Selected step for detail modal view
10 selectedStepForDetail: null,
11
12 // ACE editor instance for raw JSON view
13 _rawEditor: null,
14
15 // Show step detail modal
16 showStepDetail(stepData) {
17 if (!stepData) return;
18 this.selectedStepForDetail = stepData;
19 window.openModal("modals/process-step-detail/process-step-detail.html");
20 },
21
22 // render copy buttons for each segment (called via x-init)
23 renderSegmentButtons() {
24 const step = this.selectedStepForDetail;
25
26 [
27 { segment: "heading", value: this.cleanHeading(step?.heading) },
28 { segment: "content", value: step?.content ?? "" }
29 ].forEach(({ segment, value }) => {
30 document.querySelector(`[data-segment="${segment}"]`)?.replaceChildren(
31 createActionButton("copy", "", () => copyToClipboard(value))
32 );
33 });
34 },
35
36 // render copy buttons for individual kvp boxes
37 renderKvpCopyButton(container, key, value) {
38 const copyText = this.formatFlatValue(value);
39 container?.replaceChildren(
40 createActionButton("copy", "", () => copyToClipboard(copyText))
41 );
42 },
43
44 // Close step detail modal
45 closeStepDetail() {
46 this.selectedStepForDetail = null;
47 window.closeModal();
48 },
49
50 // Copy text to clipboard with toast feedback
51 copyToClipboard(text) {
52 navigator.clipboard.writeText(text)
53 .then(() => notificationStore.addFrontendToastOnly("success", "Copied to clipboard!", "", 3))
54 .catch((err) => console.error("Clipboard copy failed:", err));
55 },
56
57 // Format step data for full copy (all metadata + content)
58 formatStepForCopy(step) {
59 if (!step) return "";
60 const lines = [];
61 lines.push(`Type: ${step.type || "unknown"}`);
62 const heading = this.cleanHeading(step.heading);
63 if (heading) lines.push(`Heading: ${heading}`);
64 if (step.timestamp) {
65 const date = new Date(parseFloat(step.timestamp) * 1000);
66 lines.push(`Timestamp: ${formatDateTime(date.toISOString(), "full")}`);
67 }
68 if (step.durationMs) lines.push(`Duration: ${step.durationMs}ms`);
69 if (step.kvps) {
70 lines.push("");
71 lines.push("--- Data ---");
72 const flattenedKvps = this.flattenKvps(step.kvps);
73 for (const [key, value] of Object.entries(flattenedKvps)) {
74 lines.push(this.buildKvpCopyText(key, value));
75 }
76 }
77 if (step.content) {
78 lines.push("");
79 lines.push("--- Content ---");
80 lines.push(step.content);
81 }
82 return lines.join("\n");
83 },
84
85 // Get primary content for a step (type-aware)
86 // For "Copy Content" button - returns the main content, not metadata
87 getStepPrimaryContent(step) {
88 if (!step) return "";
89 if (step.type === "code_exe") {
90 return step.content || "";
91 }
92 if (step.type === "agent") {
93 // Raw LLM response is in content field - this is the primary content
94 return step.content || "";
95 }
96 if ((step.type === "tool" || step.type === "mcp") && step.kvps) {
97 return step.kvps.result || step.content || "";
98 }
99 return step.content || "";
100 },
101
102 // Initialize ACE editor for raw JSON view
103 initRawEditor() {
104 const container = document.getElementById("step-detail-raw-editor");
105 if (!container) return;
106
107 this.destroyRawEditor();
108
109 if (!window.ace?.edit) {
110 console.warn("ACE editor not available");
111 return;
112 }
113
114 const stepData = this.selectedStepForDetail;
115 if (!stepData) return;
116
117 const editorInstance = window.ace.edit("step-detail-raw-editor");
118 if (!editorInstance) return;
119
120 this._rawEditor = editorInstance;
121
122 const darkMode = window.localStorage?.getItem("darkMode");
123 const theme = darkMode !== "false" ? "ace/theme/github_dark" : "ace/theme/tomorrow";
124
125 this._rawEditor.setTheme(theme);
126 this._rawEditor.session.setMode("ace/mode/json");
127 this._rawEditor.setValue(JSON.stringify(stepData, null, 2), -1);
128 this._rawEditor.setReadOnly(true);
129 this._rawEditor.clearSelection();
130 this._rawEditor.setOptions({
131 showPrintMargin: false,
132 highlightActiveLine: false,
133 highlightGutterLine: false
134 });
135 },
136
137 // Destroy ACE editor instance
138 destroyRawEditor() {
139 if (this._rawEditor?.destroy) {
140 this._rawEditor.destroy();
141 this._rawEditor = null;
142 }
143 },
144
145 // Format step type for display
146 formatStepType(type) {
147 const typeMap = {
148 'agent': 'Generation',
149 'code_exe': 'Code Execution',
150 'tool': 'Tool Call',
151 'mcp': 'MCP Tool',
152 'browser': 'Browser',
153 'response': 'Response',
154 'info': 'Info',
155 'hint': 'Hint',
156 'warning': 'Warning',
157 'error': 'Error',
158 'util': 'Utility',
159 'progress': 'Progress'
160 };
161 return typeMap[type] || (type ? type.charAt(0).toUpperCase() + type.slice(1) : 'Unknown');
162 },
163
164 // Format timestamp for display
165 formatTimestamp(timestamp) {
166 if (!timestamp) return '';
167 const date = new Date(parseFloat(timestamp) * 1000);
168 const hours = String(date.getHours()).padStart(2, '0');
169 const minutes = String(date.getMinutes()).padStart(2, '0');
170 const seconds = String(date.getSeconds()).padStart(2, '0');
171 return `${hours}:${minutes}:${seconds}`;
172 },
173
174 // Format duration for display
175 formatDuration(ms) {
176 if (!ms) return '';
177 if (ms < 1000) return `${ms}ms`;
178 const seconds = Math.floor(ms / 1000);
179 if (seconds < 60) return `${seconds}s`;
180 const minutes = Math.floor(seconds / 60);
181 const remainingSeconds = seconds % 60;
182 return `${minutes}m ${remainingSeconds}s`;
183 },
184
185 // Format value for display (handles objects)
186 formatValue(value) {
187 return value == null
188 ? ''
189 : (typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value));
190 },
191
192 // Format key for display (title case)
193 formatKey(key) {
194 return key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
195 },
196
197 // flatten nested kvps into top-level entries
198 flattenKvps(kvps, parentKey = "") {
199 const isPlainObject = value => value && typeof value === "object" && !Array.isArray(value);
200 return Object.entries(kvps || {}).reduce((acc, [key, value]) => {
201 const fullKey = [parentKey, this.formatKey(key)].filter(Boolean).join(" / ");
202 return {
203 ...acc,
204 ...(isPlainObject(value) ? this.flattenKvps(value, fullKey) : { [fullKey]: value })
205 };
206 }, {});
207 },
208
209 // format values
210 formatFlatValue(value) {
211 return Array.isArray(value)
212 ? value
213 .map(item => this.formatValue(item))
214 .filter(item => item.trim())
215 .join('\n')
216 : this.formatValue(value);
217 },
218
219 // build copy text
220 buildKvpCopyText(key, value) {
221 const formattedValue = this.formatFlatValue(value);
222 const separator = formattedValue.includes("\n") ? ":\n" : ": ";
223 return `${key}${separator}${formattedValue}`;
224 },
225
226 // Clean heading by removing icon:// prefixes
227 cleanHeading(text) {
228 if (!text) return "";
229 return String(text)
230 .replace(/icon:\/\/[a-zA-Z0-9_]+\s*/g, "")
231 .trim();
232 }
233 };
234
235 export const store = createStore("stepDetail", model);