Skip to content

Commit 4afe6a3

Browse files
committed
open html view icon
1 parent 37c10b3 commit 4afe6a3

3 files changed

Lines changed: 157 additions & 7 deletions

File tree

curator/media/sidebar.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,17 +109,17 @@
109109
} else {
110110
if (status) status.textContent = msg.filename ? ('Results for: ' + msg.filename) : '';
111111
if (resultsEl) {
112-
resultsEl.innerHTML = '<div class="file">\n <div class="filename">' + (msg.filename ? msg.filename : '') + '<span class="actions"><button class="open-file" data-file="' + escapeHtml(msg.filename || '') + '" title="Open file">\n<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 3H6a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h12a2 2 0 0 0 2-2V9l-6-6zm1 7V4.5L18.5 10H15z"/></svg></button></span></div>\n <div class="code">' + renderTokens(msg.tokenScores || []) + '</div>\n</div>';
112+
resultsEl.innerHTML = '<div class="file collapsed">\n <div class="filename">' + (msg.filename ? msg.filename : '') + '<span class="actions"><button class="open-file" data-file="' + escapeHtml(msg.filename || '') + '" title="Open file">\n<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 3H6a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h12a2 2 0 0 0 2-2V9l-6-6zm1 7V4.5L18.5 10H15z"/></svg></button><button class="open-html" data-file="' + escapeHtml(msg.filename || '') + '" title="Open HTML view">\n<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 5h18v2H3V5zm0 6h18v2H3v-2zm0 6h18v2H3v-2z"/></svg></button></span></div>\n <div class="code">' + renderTokens(msg.tokenScores || []) + '</div>\n</div>';
113113
animateTokens(resultsEl.querySelector('.file .code'));
114114
}
115115
}
116116
} else if (msg.type === 'renderFile') {
117117
if (!resultsEl) return;
118118
const safeName = escapeHtml(msg.filename || '');
119119
if (msg.error) {
120-
resultsEl.innerHTML += '<div class="file">\n <div class="filename">' + safeName + '<span class="actions"><button class="open-file" data-file="' + safeName + '" title="Open file">\n<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 3H6a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h12a2 2 0 0 0 2-2V9l-6-6zm1 7V4.5L18.5 10H15z"/></svg></button></span></div>\n <div class="error">' + escapeHtml(String(msg.error)) + '</div>\n</div>';
120+
resultsEl.innerHTML += '<div class="file collapsed">\n <div class="filename">' + safeName + '<span class="actions"><button class="open-file" data-file="' + safeName + '" title="Open file">\n<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 3H6a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h12a2 2 0 0 0 2-2V9l-6-6zm1 7V4.5L18.5 10H15z"/></svg></button><button class="open-html" data-file="' + safeName + '" title="Open HTML view">\n<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 5h18v2H3V5zm0 6h18v2H3v-2zm0 6h18v2H3v-2z"/></svg></button></span></div>\n <div class="error">' + escapeHtml(String(msg.error)) + '</div>\n</div>';
121121
} else {
122-
resultsEl.innerHTML += '<div class="file">\n <div class="filename">' + safeName + '<span class="actions"><button class="open-file" data-file="' + safeName + '" title="Open file">\n<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 3H6a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h12a2 2 0 0 0 2-2V9l-6-6zm1 7V4.5L18.5 10H15z"/></svg></button></span></div>\n <div class="code">' + renderTokens(msg.tokenScores || []) + '</div>\n</div>';
122+
resultsEl.innerHTML += '<div class="file collapsed">\n <div class="filename">' + safeName + '<span class="actions"><button class="open-file" data-file="' + safeName + '" title="Open file">\n<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14 3H6a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h12a2 2 0 0 0 2-2V9l-6-6zm1 7V4.5L18.5 10H15z"/></svg></button><button class="open-html" data-file="' + safeName + '" title="Open HTML view">\n<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 5h18v2H3V5zm0 6h18v2H3v-2zm0 6h18v2H3v-2z"/></svg></button></span></div>\n <div class="code">' + renderTokens(msg.tokenScores || []) + '</div>\n</div>';
123123
animateTokens(resultsEl.lastElementChild && resultsEl.lastElementChild.querySelector('.code'));
124124
}
125125
} else if (msg.type === 'status') {
@@ -161,13 +161,21 @@
161161
const target = e.target;
162162
if (!(target instanceof Element)) return;
163163
const openBtn = (target.closest && target.closest('.open-file')) ? target.closest('.open-file') : null;
164+
const openHtmlBtn = (target.closest && target.closest('.open-html')) ? target.closest('.open-html') : null;
164165
if (openBtn) {
165166
const filename = openBtn.getAttribute('data-file');
166167
if (filename) {
167168
try { vscode.postMessage({ type: 'openFile', filename }); } catch (e) {}
168169
}
169170
return;
170171
}
172+
if (openHtmlBtn) {
173+
const filename = openHtmlBtn.getAttribute('data-file');
174+
if (filename) {
175+
try { vscode.postMessage({ type: 'openHtml', filename }); } catch (e) {}
176+
}
177+
return;
178+
}
171179
const header = target.closest('.filename');
172180
if (header) {
173181
const fileEl = header.parentElement;

curator/media/tokenView.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
(function(){
2+
function escapeHtml(text) {
3+
return String(text)
4+
.replace(/&/g, '&amp;')
5+
.replace(/</g, '&lt;')
6+
.replace(/>/g, '&gt;')
7+
.replace(/"/g, '&quot;')
8+
.replace(/'/g, '&#039;');
9+
}
10+
11+
function clamp01(value){
12+
if (isNaN(value)) return 0;
13+
if (value < 0) return 0;
14+
if (value > 1) return 1;
15+
return value;
16+
}
17+
18+
function scoreToColor(score){
19+
const clamped = clamp01(score);
20+
const hue = 120 - Math.round(clamped * 120);
21+
return 'hsl(' + hue + ', 85%, 40%)';
22+
}
23+
24+
function renderTokens(tokenScores){
25+
const parts = [];
26+
for (const item of (tokenScores || [])){
27+
const token = escapeHtml(item && item.token != null ? item.token : '');
28+
const score = item ? item.score : null;
29+
const reason = item ? item.reason : null;
30+
let style = '';
31+
let title = '';
32+
if (score !== null && score !== undefined){
33+
style = 'background:' + scoreToColor(score) + ';';
34+
title = 'score: ' + clamp01(score).toFixed(3) + (reason ? '\nreason: ' + String(reason) : '');
35+
}
36+
parts.push('<span class="tok" data-tip="' + escapeHtml(title) + '" style="' + style + '">' + token + '</span>');
37+
}
38+
return parts.join('');
39+
}
40+
41+
function showTooltip(tooltip, text, x, y){
42+
if (!tooltip) return;
43+
if (!text){ hideTooltip(tooltip); return; }
44+
tooltip.textContent = text;
45+
tooltip.style.left = (x + 10) + 'px';
46+
tooltip.style.top = (y + 10) + 'px';
47+
tooltip.classList.add('visible');
48+
tooltip.setAttribute('aria-hidden', 'false');
49+
}
50+
51+
function hideTooltip(tooltip){
52+
if (!tooltip) return;
53+
tooltip.classList.remove('visible');
54+
tooltip.setAttribute('aria-hidden', 'true');
55+
}
56+
57+
document.addEventListener('DOMContentLoaded', function(){
58+
const dataEl = document.getElementById('tokenData');
59+
const codeEl = document.getElementById('code');
60+
const tooltip = document.getElementById('tooltip');
61+
try{
62+
const tokenScores = JSON.parse(dataEl ? dataEl.textContent || '[]' : '[]');
63+
if (codeEl) codeEl.innerHTML = renderTokens(tokenScores);
64+
if (codeEl){
65+
codeEl.addEventListener('mousemove', function(e){
66+
const t = e.target;
67+
if (!(t instanceof Element)) return;
68+
if (t.classList.contains('tok')){
69+
const tip = t.getAttribute('data-tip') || '';
70+
showTooltip(tooltip, tip, e.clientX, e.clientY);
71+
} else {
72+
hideTooltip(tooltip);
73+
}
74+
});
75+
codeEl.addEventListener('mouseleave', function(){ hideTooltip(tooltip); });
76+
}
77+
} catch (e) {
78+
if (codeEl) codeEl.textContent = 'Failed to render tokens.';
79+
}
80+
});
81+
})();
82+
83+

curator/src/sidebar/provider.ts

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import * as vscode from 'vscode';
22
import { getAllUncommittedFileDiffs, UncommittedDiffResult } from '../services/gitService';
3-
import { postFileDiff } from '../services/networkClient';
3+
import { postFileDiff, type TokenScore } from '../services/networkClient';
44

55
export class CuratorViewProvider implements vscode.WebviewViewProvider {
66
public static readonly viewType = 'curatorView';
77

88
private _view: vscode.WebviewView | undefined;
99
private readonly _extensionUri: vscode.Uri;
1010
private _lastDiffs: UncommittedDiffResult[] = [];
11+
private _lastTokenScoresByFile: Map<string, TokenScore[]> = new Map();
1112

1213
constructor(extensionUri: vscode.Uri) {
1314
this._extensionUri = extensionUri;
@@ -31,6 +32,13 @@ export class CuratorViewProvider implements vscode.WebviewViewProvider {
3132
console.log('[Curator] Failed to open file', e);
3233
}
3334
}
35+
} else if (msg?.type === 'openHtml' && typeof msg.filename === 'string') {
36+
const tokens = this._lastTokenScoresByFile.get(msg.filename);
37+
if (tokens && tokens.length > 0) {
38+
await this.openHtmlTokenView(msg.filename, tokens);
39+
} else {
40+
void vscode.window.showWarningMessage('Curator: No tokens available for ' + msg.filename);
41+
}
3442
}
3543
});
3644
}
@@ -70,9 +78,9 @@ export class CuratorViewProvider implements vscode.WebviewViewProvider {
7078
.tooltip.visible { opacity: 1; }
7179
7280
.actions { display: inline-flex; align-items: center; gap: 6px; }
73-
.open-file { background: none; border: none; padding: 2px; cursor: pointer; color: var(--vscode-foreground); opacity: 0.8; }
74-
.open-file:hover { opacity: 1; }
75-
.open-file svg { width: 14px; height: 14px; display: block; fill: currentColor; }
81+
.open-file, .open-html { background: none; border: none; padding: 2px; cursor: pointer; color: var(--vscode-foreground); opacity: 0.8; }
82+
.open-file:hover, .open-html:hover { opacity: 1; }
83+
.open-file svg, .open-html svg { width: 14px; height: 14px; display: block; fill: currentColor; }
7684
7785
.progress { margin-top: 6px; font-size: 12px; color: var(--vscode-descriptionForeground); display: flex; align-items: center; gap: 8px; }
7886
.progress .bar { flex: 1; height: 6px; background: var(--vscode-editorWidget-border); border-radius: 3px; overflow: hidden; }
@@ -127,6 +135,7 @@ export class CuratorViewProvider implements vscode.WebviewViewProvider {
127135
const doc = await vscode.workspace.openTextDocument(diffResult.fileUri);
128136
const fileText = doc.getText();
129137
const response = await postFileDiff(diffResult.diffText, fileText);
138+
this._lastTokenScoresByFile.set(diffResult.relativePath, response.tokenScores);
130139
await view.webview.postMessage({ type: 'renderFile', filename: diffResult.relativePath, tokenScores: response.tokenScores });
131140
} catch (err) {
132141
const message = err instanceof Error ? err.message : 'Failed to analyze diff.';
@@ -142,6 +151,56 @@ export class CuratorViewProvider implements vscode.WebviewViewProvider {
142151
await view.webview.postMessage({ type: 'loading', value: false });
143152
}
144153
}
154+
155+
private async openHtmlTokenView(title: string, tokenScores: TokenScore[]): Promise<void> {
156+
const panel = vscode.window.createWebviewPanel('curatorTokens', `Curator: ${title}`, vscode.ViewColumn.Active, { enableScripts: true, retainContextWhenHidden: false });
157+
const jsUri = panel.webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'media', 'tokenView.js'));
158+
const csp = `default-src 'none'; style-src ${panel.webview.cspSource} 'unsafe-inline'; script-src ${panel.webview.cspSource};`;
159+
const safeJson = JSON.stringify(tokenScores)
160+
.replace(/</g, '\\u003c')
161+
.replace(/>/g, '\\u003e')
162+
.replace(/&/g, '\\u0026');
163+
panel.webview.html = `<!DOCTYPE html>
164+
<html lang="en">
165+
<head>
166+
<meta charset="UTF-8" />
167+
<meta http-equiv="Content-Security-Policy" content="${csp}" />
168+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
169+
<title>Curator: ${this.escapeHtml(title)}</title>
170+
<style>
171+
body { font-family: var(--vscode-font-family); margin: 0; padding: 12px; }
172+
.code { white-space: pre-wrap; word-wrap: break-word; line-height: 1.6; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12.5px; }
173+
.tok { display: inline; padding: 1px 2px; border-radius: 3px; color: #fff; }
174+
.tooltip { position: fixed; z-index: 1000; background: var(--vscode-editorWidget-background); color: var(--vscode-foreground); border: 1px solid var(--vscode-editorWidget-border); border-radius: 4px; padding: 6px 8px; font-size: 12px; box-shadow: 0 2px 6px rgba(0,0,0,0.2); pointer-events: none; opacity: 0; transition: opacity 0.05s ease-in-out; max-width: 360px; white-space: pre-wrap; }
175+
.tooltip.visible { opacity: 1; }
176+
</style>
177+
</head>
178+
<body>
179+
<div class="code" id="code"></div>
180+
<div id="tooltip" class="tooltip" aria-hidden="true"></div>
181+
<script id="tokenData" type="application/json">${safeJson}</script>
182+
<script src="${jsUri}"></script>
183+
</body>
184+
</html>`;
185+
}
186+
187+
private generateNonce(): string {
188+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
189+
let out = '';
190+
for (let i = 0; i < 16; i++) {
191+
out += chars.charAt(Math.floor(Math.random() * chars.length));
192+
}
193+
return out;
194+
}
195+
196+
private escapeHtml(text: string): string {
197+
return String(text)
198+
.replace(/&/g, '&amp;')
199+
.replace(/</g, '&lt;')
200+
.replace(/>/g, '&gt;')
201+
.replace(/"/g, '&quot;')
202+
.replace(/'/g, '&#039;');
203+
}
145204
}
146205

147206

0 commit comments

Comments
 (0)