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 <[email protected]>
- 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 |
});
|