1<div class="px-6 py-4" data-exception-container>
2 <div class="flex justify-between items-center mb-4">
3 <div class="text-amber-400" data-exception-message>
4 <span class="font-semibold">{{ exception_context.exception.__class__.__name__ }}</span>: {{ exception_context.exception }}
5 </div>
6 <div class="flex items-center space-x-2 ml-4">
7 <button data-toggle-raw class="flex items-center space-x-1 px-2 py-0.5 text-sm text-stone-300 rounded bg-white/10 hover:bg-white/20 transition-colors cursor-pointer">
8 <span>View raw</span>
9 </button>
10 <button data-copy-exception class="flex items-center space-x-1 px-2 py-0.5 text-sm text-stone-300 rounded bg-white/10 hover:bg-white/20 transition-colors cursor-pointer">
11 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="w-3.5 h-3.5 bi bi-clipboard" viewBox="0 0 16 16">
12 <path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
13 <path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
14 </svg>
15 <span>Copy</span>
16 </button>
17 </div>
18 </div>
19
20 {# Traceback frames with source code #}
21 <div class="space-y-1" data-exception-frames>
22 {% for frame in exception_context.traceback_frames %}
23 <div
24 data-category="{{ frame.category }}"
25 data-expanded="{{ 'true' if frame.category == 'app' else 'false' }}"
26 class="group rounded overflow-hidden border border-white/10 bg-white/5 data-[category=app]:bg-stone-500/10"
27 >
28 <button
29 data-toggle-frame
30 class="w-full px-2 py-1 text-xs flex justify-between items-center text-stone-400 hover:text-stone-300 hover:bg-white/5 cursor-pointer group-data-[category=app]:text-stone-300"
31 >
32 <div class="flex items-center gap-2">
33 <svg data-frame-chevron class="w-3 h-3 transition-transform group-data-[expanded=false]:-rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24">
34 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
35 </svg>
36 <span data-category="{{ frame.category }}" class="px-1.5 py-0.5 rounded text-[10px] font-medium
37 data-[category=app]:bg-stone-500/30 data-[category=app]:text-stone-200
38 data-[category=plain]:bg-emerald-500/30 data-[category=plain]:text-emerald-300
39 data-[category=plainx]:bg-violet-500/30 data-[category=plainx]:text-violet-300
40 data-[category=python]:bg-sky-500/30 data-[category=python]:text-sky-300
41 data-[category=third-party]:bg-neutral-500/30 data-[category=third-party]:text-neutral-300"
42 >{{ frame.category }}</span>
43 <a href="vscode://file/{{ frame.filename }}:{{ frame.lineno }}" data-vscode-link class="font-mono hover:underline text-stone-500">{{ frame.filename }}</a>
44 </div>
45 <span>line {{ frame.lineno }} in <span class="font-semibold">{{ frame.name }}</span></span>
46 </button>
47 <div data-frame-content data-visible="{{ 'true' if frame.category == 'app' else 'false' }}" class="data-[visible=false]:hidden">
48 {% if frame.source_lines %}
49 <div class="text-xs overflow-auto">
50 <table class="w-full">
51 {% for line in frame.source_lines %}
52 <tr data-error-line="{{ line.is_error_line|lower }}" class="data-[error-line=true]:bg-amber-500/20">
53 <td class="px-2 py-0.5 text-right text-stone-500 select-none w-12 font-mono">{{ line.lineno }}</td>
54 <td class="px-2 py-0.5 font-mono whitespace-pre text-stone-300">{{ line.code }}</td>
55 </tr>
56 {% endfor %}
57 </table>
58 </div>
59 {% endif %}
60 {% if frame.locals %}
61 <div class="border-t border-white/10">
62 <table class="text-xs">
63 {% for var in frame.locals %}
64 <tr class="hover:bg-white/5">
65 <td class="py-0.5 pl-2 pr-4 text-stone-400 font-mono whitespace-nowrap">{{ var.name }}</td>
66 <td class="py-0.5 pr-4 text-stone-500 whitespace-nowrap">{{ var.type }}</td>
67 <td class="py-0.5 pr-2 text-stone-400 font-mono">{{ var.value }}</td>
68 </tr>
69 {% endfor %}
70 </table>
71 </div>
72 {% endif %}
73 </div>
74 </div>
75 {% endfor %}
76 </div>
77
78 {# Raw traceback (hidden by default, toggled with Raw button) #}
79 <div class="hidden mt-4 text-stone-300 text-xs bg-white/5 p-2 rounded overflow-auto" data-exception-traceback>
80 <pre><code>{{ exception_context.traceback_string }}</code></pre>
81 </div>
82</div>
83
84<script nonce="{{ request.csp_nonce }}">
85(function() {
86 function copyException(button) {
87 const container = button.closest('[data-exception-container]');
88 const exceptionText = container.querySelector('[data-exception-message]').textContent.trim();
89 const traceback = container.querySelector('[data-exception-traceback] code').textContent;
90 const fullText = `Exception: ${exceptionText}\n\n${traceback}`;
91
92 navigator.clipboard.writeText(fullText).then(() => {
93 // Change icon and text to indicate success
94 button.innerHTML = `
95 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="w-3.5 h-3.5 bi bi-check2" viewBox="0 0 16 16">
96 <path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
97 </svg>
98 <span>Copied!</span>
99 `;
100
101 // Reset after 2 seconds
102 setTimeout(() => {
103 button.innerHTML = `
104 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="w-3.5 h-3.5 bi bi-clipboard" viewBox="0 0 16 16">
105 <path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
106 <path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
107 </svg>
108 <span>Copy</span>
109 `;
110 }, 2000);
111 }).catch(err => {
112 console.error('Failed to copy exception:', err);
113 alert('Failed to copy exception to clipboard');
114 });
115 }
116
117 // Set up copy event listener
118 const copyButton = document.querySelector('[data-copy-exception]');
119 if (copyButton) {
120 copyButton.addEventListener('click', function() {
121 copyException(this);
122 });
123 }
124
125 // Set up raw toggle event listener
126 const toggleButton = document.querySelector('[data-toggle-raw]');
127 if (toggleButton) {
128 toggleButton.addEventListener('click', function() {
129 const container = this.closest('[data-exception-container]');
130 const frames = container.querySelector('[data-exception-frames]');
131 const raw = container.querySelector('[data-exception-traceback]');
132
133 frames.classList.toggle('hidden');
134 raw.classList.toggle('hidden');
135
136 // Update button text
137 const isRawVisible = !raw.classList.contains('hidden');
138 this.querySelector('span').textContent = isRawVisible ? 'View frames' : 'View raw';
139 });
140 }
141
142 // Set up frame toggle event listeners
143 document.querySelectorAll('[data-toggle-frame]').forEach(btn => {
144 btn.addEventListener('click', function() {
145 const frame = this.closest('[data-expanded]');
146 const content = frame.querySelector('[data-frame-content]');
147 const isExpanded = frame.dataset.expanded === 'true';
148
149 frame.dataset.expanded = isExpanded ? 'false' : 'true';
150 content.dataset.visible = isExpanded ? 'false' : 'true';
151 });
152 });
153
154 // Prevent VS Code links from toggling frame
155 document.querySelectorAll('[data-vscode-link]').forEach(link => {
156 link.addEventListener('click', function(e) {
157 e.stopPropagation();
158 });
159 });
160})();
161</script>