Spaces:
Running
Running
Commit
·
69abcd1
1
Parent(s):
fae80e2
Add deep link sharing functionality
Browse files- Read URL parameters on page load (dataset, index, view, diff, markdown)
- Auto-update URL when navigating or changing settings
- Add Copy Link button with success notification
- Support fallback clipboard API for older browsers
- Enable sharing specific views with full state preservation
Users can now share direct links to specific pages and views,
making collaboration and reference sharing much easier.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- index.html +27 -0
- js/app.js +102 -10
index.html
CHANGED
|
@@ -144,6 +144,33 @@
|
|
| 144 |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
| 145 |
</svg>
|
| 146 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
</div>
|
| 148 |
</div>
|
| 149 |
</div>
|
|
|
|
| 144 |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
| 145 |
</svg>
|
| 146 |
</button>
|
| 147 |
+
|
| 148 |
+
<!-- Copy Link Button -->
|
| 149 |
+
<div class="relative">
|
| 150 |
+
<button
|
| 151 |
+
@click="copyShareLink()"
|
| 152 |
+
class="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
| 153 |
+
title="Copy shareable link"
|
| 154 |
+
>
|
| 155 |
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 156 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>
|
| 157 |
+
</svg>
|
| 158 |
+
</button>
|
| 159 |
+
|
| 160 |
+
<!-- Success Toast -->
|
| 161 |
+
<div
|
| 162 |
+
x-show="showShareSuccess"
|
| 163 |
+
x-transition
|
| 164 |
+
class="absolute right-0 top-12 bg-green-600 text-white px-3 py-2 rounded-md shadow-lg whitespace-nowrap z-50"
|
| 165 |
+
>
|
| 166 |
+
<div class="flex items-center gap-2">
|
| 167 |
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 168 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
| 169 |
+
</svg>
|
| 170 |
+
<span class="text-sm">Link copied!</span>
|
| 171 |
+
</div>
|
| 172 |
+
</div>
|
| 173 |
+
</div>
|
| 174 |
</div>
|
| 175 |
</div>
|
| 176 |
</div>
|
js/app.js
CHANGED
|
@@ -33,6 +33,7 @@ document.addEventListener('alpine:init', () => {
|
|
| 33 |
showDock: false,
|
| 34 |
renderMarkdown: false,
|
| 35 |
hasMarkdown: false,
|
|
|
|
| 36 |
|
| 37 |
// Reasoning trace state
|
| 38 |
hasReasoningTrace: false,
|
|
@@ -76,6 +77,31 @@ document.addEventListener('alpine:init', () => {
|
|
| 76 |
// Initialize API
|
| 77 |
this.api = new DatasetAPI();
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
// Apply dark mode from localStorage
|
| 80 |
this.darkMode = localStorage.getItem('darkMode') === 'true';
|
| 81 |
this.$watch('darkMode', value => {
|
|
@@ -87,8 +113,19 @@ document.addEventListener('alpine:init', () => {
|
|
| 87 |
// Setup keyboard navigation
|
| 88 |
this.setupKeyboardNavigation();
|
| 89 |
|
|
|
|
|
|
|
|
|
|
| 90 |
// Load initial dataset
|
| 91 |
await this.loadDataset();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
},
|
| 93 |
|
| 94 |
setupKeyboardNavigation() {
|
|
@@ -208,10 +245,7 @@ document.addEventListener('alpine:init', () => {
|
|
| 208 |
this.updateDiff();
|
| 209 |
|
| 210 |
// Update URL without triggering navigation
|
| 211 |
-
|
| 212 |
-
url.searchParams.set('dataset', this.datasetId);
|
| 213 |
-
url.searchParams.set('index', index);
|
| 214 |
-
window.history.replaceState({}, '', url);
|
| 215 |
|
| 216 |
} catch (error) {
|
| 217 |
this.error = `Failed to load sample: ${error.message}`;
|
|
@@ -811,15 +845,73 @@ document.addEventListener('alpine:init', () => {
|
|
| 811 |
await this.loadSample(index);
|
| 812 |
},
|
| 813 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 814 |
// Watch for diff mode changes
|
| 815 |
initWatchers() {
|
| 816 |
-
this.$watch('diffMode', () =>
|
|
|
|
|
|
|
|
|
|
| 817 |
this.$watch('currentSample', () => this.updateDiff());
|
|
|
|
|
|
|
| 818 |
}
|
| 819 |
}));
|
| 820 |
-
});
|
| 821 |
-
|
| 822 |
-
// Initialize watchers after Alpine loads
|
| 823 |
-
document.addEventListener('alpine:initialized', () => {
|
| 824 |
-
Alpine.store('ocrExplorer')?.initWatchers?.();
|
| 825 |
});
|
|
|
|
| 33 |
showDock: false,
|
| 34 |
renderMarkdown: false,
|
| 35 |
hasMarkdown: false,
|
| 36 |
+
showShareSuccess: false,
|
| 37 |
|
| 38 |
// Reasoning trace state
|
| 39 |
hasReasoningTrace: false,
|
|
|
|
| 77 |
// Initialize API
|
| 78 |
this.api = new DatasetAPI();
|
| 79 |
|
| 80 |
+
// Read URL parameters for deep linking
|
| 81 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 82 |
+
const urlDataset = urlParams.get('dataset');
|
| 83 |
+
const urlIndex = urlParams.get('index');
|
| 84 |
+
const urlView = urlParams.get('view');
|
| 85 |
+
const urlDiff = urlParams.get('diff');
|
| 86 |
+
const urlMarkdown = urlParams.get('markdown');
|
| 87 |
+
|
| 88 |
+
// Apply URL parameters if present
|
| 89 |
+
if (urlDataset) {
|
| 90 |
+
this.datasetId = urlDataset;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
if (urlView && ['comparison', 'diff', 'improved'].includes(urlView)) {
|
| 94 |
+
this.activeTab = urlView;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
if (urlDiff && ['char', 'word', 'line', 'markdown'].includes(urlDiff)) {
|
| 98 |
+
this.diffMode = urlDiff;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
if (urlMarkdown !== null) {
|
| 102 |
+
this.renderMarkdown = urlMarkdown === 'true';
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
// Apply dark mode from localStorage
|
| 106 |
this.darkMode = localStorage.getItem('darkMode') === 'true';
|
| 107 |
this.$watch('darkMode', value => {
|
|
|
|
| 113 |
// Setup keyboard navigation
|
| 114 |
this.setupKeyboardNavigation();
|
| 115 |
|
| 116 |
+
// Setup watchers for URL updates
|
| 117 |
+
this.initWatchers();
|
| 118 |
+
|
| 119 |
// Load initial dataset
|
| 120 |
await this.loadDataset();
|
| 121 |
+
|
| 122 |
+
// Jump to specific index if provided in URL
|
| 123 |
+
if (urlIndex !== null) {
|
| 124 |
+
const index = parseInt(urlIndex);
|
| 125 |
+
if (!isNaN(index) && index >= 0 && index < this.totalSamples) {
|
| 126 |
+
await this.loadSample(index);
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
},
|
| 130 |
|
| 131 |
setupKeyboardNavigation() {
|
|
|
|
| 245 |
this.updateDiff();
|
| 246 |
|
| 247 |
// Update URL without triggering navigation
|
| 248 |
+
this.updateURL();
|
|
|
|
|
|
|
|
|
|
| 249 |
|
| 250 |
} catch (error) {
|
| 251 |
this.error = `Failed to load sample: ${error.message}`;
|
|
|
|
| 845 |
await this.loadSample(index);
|
| 846 |
},
|
| 847 |
|
| 848 |
+
// Update URL with current state
|
| 849 |
+
updateURL() {
|
| 850 |
+
const url = new URL(window.location);
|
| 851 |
+
url.searchParams.set('dataset', this.datasetId);
|
| 852 |
+
url.searchParams.set('index', this.currentIndex);
|
| 853 |
+
url.searchParams.set('view', this.activeTab);
|
| 854 |
+
url.searchParams.set('diff', this.diffMode);
|
| 855 |
+
url.searchParams.set('markdown', this.renderMarkdown);
|
| 856 |
+
window.history.replaceState({}, '', url);
|
| 857 |
+
},
|
| 858 |
+
|
| 859 |
+
// Copy shareable link to clipboard
|
| 860 |
+
async copyShareLink() {
|
| 861 |
+
const url = new URL(window.location);
|
| 862 |
+
url.searchParams.set('dataset', this.datasetId);
|
| 863 |
+
url.searchParams.set('index', this.currentIndex);
|
| 864 |
+
url.searchParams.set('view', this.activeTab);
|
| 865 |
+
url.searchParams.set('diff', this.diffMode);
|
| 866 |
+
url.searchParams.set('markdown', this.renderMarkdown);
|
| 867 |
+
|
| 868 |
+
const shareUrl = url.toString();
|
| 869 |
+
|
| 870 |
+
try {
|
| 871 |
+
await navigator.clipboard.writeText(shareUrl);
|
| 872 |
+
|
| 873 |
+
// Show success feedback
|
| 874 |
+
this.showShareSuccess = true;
|
| 875 |
+
setTimeout(() => {
|
| 876 |
+
this.showShareSuccess = false;
|
| 877 |
+
}, 2000);
|
| 878 |
+
|
| 879 |
+
return true;
|
| 880 |
+
} catch (err) {
|
| 881 |
+
// Fallback for older browsers
|
| 882 |
+
const textArea = document.createElement('textarea');
|
| 883 |
+
textArea.value = shareUrl;
|
| 884 |
+
textArea.style.position = 'fixed';
|
| 885 |
+
textArea.style.opacity = '0';
|
| 886 |
+
document.body.appendChild(textArea);
|
| 887 |
+
textArea.select();
|
| 888 |
+
|
| 889 |
+
try {
|
| 890 |
+
document.execCommand('copy');
|
| 891 |
+
// Show success feedback
|
| 892 |
+
this.showShareSuccess = true;
|
| 893 |
+
setTimeout(() => {
|
| 894 |
+
this.showShareSuccess = false;
|
| 895 |
+
}, 2000);
|
| 896 |
+
return true;
|
| 897 |
+
} catch (err) {
|
| 898 |
+
console.error('Failed to copy link:', err);
|
| 899 |
+
return false;
|
| 900 |
+
} finally {
|
| 901 |
+
document.body.removeChild(textArea);
|
| 902 |
+
}
|
| 903 |
+
}
|
| 904 |
+
},
|
| 905 |
+
|
| 906 |
// Watch for diff mode changes
|
| 907 |
initWatchers() {
|
| 908 |
+
this.$watch('diffMode', () => {
|
| 909 |
+
this.updateDiff();
|
| 910 |
+
this.updateURL();
|
| 911 |
+
});
|
| 912 |
this.$watch('currentSample', () => this.updateDiff());
|
| 913 |
+
this.$watch('activeTab', () => this.updateURL());
|
| 914 |
+
this.$watch('renderMarkdown', () => this.updateURL());
|
| 915 |
}
|
| 916 |
}));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 917 |
});
|