@glossarist/concept-browser 0.7.77 → 0.7.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/astro.config.mjs +1 -0
- package/package.json +2 -2
- package/scripts/bridge-to-astro.mjs +6 -1
- package/scripts/build-edges.js +154 -1
- package/scripts/generate-data.mjs +138 -54
- package/src/__tests__/build-edges-redundancy.test.ts +101 -0
- package/src/__tests__/composables/use-concept-diff.test.ts +240 -0
- package/src/__tests__/composables/use-concept-edges-v2.test.ts +132 -0
- package/src/__tests__/composables/use-dataset-stats.test.ts +96 -0
- package/src/__tests__/config/page-types.test.ts +82 -0
- package/src/__tests__/model-bridge-bridges.test.ts +3 -2
- package/src/__tests__/model-bridge-citation.test.ts +1 -1
- package/src/__tests__/ontology-registry.test.ts +2 -2
- package/src/__tests__/partitive-relation-styling.test.ts +82 -0
- package/src/__tests__/partitive-relation.test.ts +285 -0
- package/src/__tests__/sphere-math.test.ts +56 -0
- package/src/adapters/GraphDataSource.ts +22 -5
- package/src/adapters/UriRouter.ts +5 -0
- package/src/adapters/dataset-stats.ts +13 -0
- package/src/adapters/model-bridge.ts +1 -1
- package/src/adapters/non-verbal/glossarist-augment.d.ts +155 -4
- package/src/adapters/types.ts +36 -0
- package/src/components/AppSidebar.vue +14 -57
- package/src/components/ConceptDetail.vue +29 -0
- package/src/components/ConceptDiffModal.vue +120 -0
- package/src/components/ConceptDiffView.vue +192 -0
- package/src/components/ConceptEditionRail.vue +61 -36
- package/src/components/PartitiveRelationList.vue +139 -0
- package/src/components/RelationSphere.vue +2 -36
- package/src/components/groups/DatasetGroupRenderer.vue +14 -4
- package/src/components/groups/DefaultGroupSidebar.vue +28 -11
- package/src/components/groups/LineageGroupSidebar.vue +17 -29
- package/src/composables/use-concept-diff.ts +231 -0
- package/src/composables/use-concept-edges.ts +98 -5
- package/src/composables/use-dataset-stats.ts +52 -0
- package/src/config/page-types.ts +67 -0
- package/src/config/types.ts +6 -1
- package/src/config/use-site-config.ts +13 -29
- package/src/data/taxonomies.json +24 -1
- package/src/i18n/locales/eng.yml +6 -0
- package/src/utils/partitive-relation-styling.ts +62 -0
- package/src/utils/sphere-math.ts +54 -0
- package/src/views/DatasetView.vue +5 -3
- package/src/views/StatsView.vue +9 -22
- package/src/islands/BibliographyIsland.vue +0 -26
- package/src/islands/ConceptDetailIsland.vue +0 -27
- package/src/islands/ConceptDiffIsland.vue +0 -214
- package/src/islands/ConceptIsland.vue +0 -102
- package/src/islands/GraphIsland.vue +0 -15
- package/src/islands/GroupRendererIsland.vue +0 -15
- package/src/islands/HeaderIsland.vue +0 -10
- package/src/islands/NonVerbalIsland.vue +0 -75
- package/src/islands/RelationSphereIsland.vue +0 -20
- package/src/islands/SearchIsland.vue +0 -23
- package/src/islands/SeriesCardIsland.vue +0 -12
- package/src/islands/ShortcutsIsland.vue +0 -13
- package/src/islands/SidebarIsland.vue +0 -15
- package/src/islands/SupersessionDiffIsland.vue +0 -267
package/src/data/taxonomies.json
CHANGED
|
@@ -45,6 +45,12 @@
|
|
|
45
45
|
"iri": "gloss:status/retired",
|
|
46
46
|
"prefLabel": "retired",
|
|
47
47
|
"definition": "The concept has been retired from active use."
|
|
48
|
+
},
|
|
49
|
+
"external": {
|
|
50
|
+
"id": "external",
|
|
51
|
+
"iri": "gloss:status/external",
|
|
52
|
+
"prefLabel": "external",
|
|
53
|
+
"definition": "An external concept: referenced but not defined here. May be resolved later when another dataset is loaded (concept-model TODO.partitive-relation-v2 item 06)."
|
|
48
54
|
}
|
|
49
55
|
},
|
|
50
56
|
"colors": {
|
|
@@ -54,7 +60,8 @@
|
|
|
54
60
|
"not_valid": "bg-red-50 text-red-700",
|
|
55
61
|
"invalid": "bg-red-50 text-red-700",
|
|
56
62
|
"superseded": "bg-red-50 text-red-700",
|
|
57
|
-
"retired": "badge-gray"
|
|
63
|
+
"retired": "badge-gray",
|
|
64
|
+
"external": "bg-purple-50 text-purple-700"
|
|
58
65
|
}
|
|
59
66
|
},
|
|
60
67
|
"entryStatus": {
|
|
@@ -264,6 +271,22 @@
|
|
|
264
271
|
"category": "lifecycle",
|
|
265
272
|
"inverseOf": "supersedes"
|
|
266
273
|
},
|
|
274
|
+
"provides": {
|
|
275
|
+
"id": "provides",
|
|
276
|
+
"iri": "gloss:rel/provides",
|
|
277
|
+
"prefLabel": "provides",
|
|
278
|
+
"definition": "This concept provides substance for an external concept (one we reference but don't define). Direction: subject → external. Per concept-model TODO.partitive-relation-v2 item 07.",
|
|
279
|
+
"category": "lifecycle",
|
|
280
|
+
"inverseOf": "provided_by"
|
|
281
|
+
},
|
|
282
|
+
"provided_by": {
|
|
283
|
+
"id": "provided_by",
|
|
284
|
+
"iri": "gloss:rel/provided_by",
|
|
285
|
+
"prefLabel": "provided by",
|
|
286
|
+
"definition": "This external concept is provided by (fulfilled by) the object concept. Inverse of provides.",
|
|
287
|
+
"category": "lifecycle",
|
|
288
|
+
"inverseOf": "provides"
|
|
289
|
+
},
|
|
267
290
|
"compare": {
|
|
268
291
|
"id": "compare",
|
|
269
292
|
"iri": "gloss:rel/compare",
|
package/src/i18n/locales/eng.yml
CHANGED
|
@@ -58,6 +58,12 @@ concept.example: Example
|
|
|
58
58
|
concept.sources: Sources
|
|
59
59
|
concept.designations: Designations
|
|
60
60
|
concept.relations: Relations
|
|
61
|
+
concept.compareWith: Compare with this edition
|
|
62
|
+
concept.compareEditions: Compare editions
|
|
63
|
+
concept.partitiveRelations: Partitive relations
|
|
64
|
+
partitive.relations: Partitive relations
|
|
65
|
+
partitive.plurality.shared: shared type
|
|
66
|
+
partitive.plurality.uncertain: uncertain
|
|
61
67
|
concept.classification: Classification
|
|
62
68
|
concept.domains: Domains
|
|
63
69
|
concept.tags: Tags
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Partitive relation styling — colors for one-to-many decompositions.
|
|
3
|
+
*
|
|
4
|
+
* Independent of relationship-categories.ts (which handles binary edges).
|
|
5
|
+
* Partitive relations have their own visual language:
|
|
6
|
+
* - completeness (complete/partial) affects opacity
|
|
7
|
+
* - plurality (type-shared, uncertain) adds accent badges
|
|
8
|
+
*
|
|
9
|
+
* v2 redesign: replaces v1 hyperedge-styling.ts per
|
|
10
|
+
* concept-model/TODO.partitive-relation-v2.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { TypeSharedPluralityWire } from '../adapters/types';
|
|
14
|
+
|
|
15
|
+
const BASE_LIGHT = '#0d9488';
|
|
16
|
+
const BASE_DARK = '#2dd4bf';
|
|
17
|
+
const ACCENT_SHARED_LIGHT = '#3b82f6';
|
|
18
|
+
const ACCENT_SHARED_DARK = '#60a5fa';
|
|
19
|
+
const ACCENT_UNCERTAIN_LIGHT = '#f59e0b';
|
|
20
|
+
const ACCENT_UNCERTAIN_DARK = '#fbbf24';
|
|
21
|
+
|
|
22
|
+
export interface PartitiveRelationStyle {
|
|
23
|
+
color: string;
|
|
24
|
+
badgeClass: string;
|
|
25
|
+
opacity: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function partitiveRelationStyle(
|
|
29
|
+
completeness: 'complete' | 'partial',
|
|
30
|
+
plurality: TypeSharedPluralityWire | null,
|
|
31
|
+
isDark: boolean,
|
|
32
|
+
): PartitiveRelationStyle {
|
|
33
|
+
const base = isDark ? BASE_DARK : BASE_LIGHT;
|
|
34
|
+
const opacity = completeness === 'partial' ? 0.6 : 1.0;
|
|
35
|
+
|
|
36
|
+
let badgeClass = 'badge-teal';
|
|
37
|
+
if (plurality?.isShared) {
|
|
38
|
+
badgeClass = 'badge-blue';
|
|
39
|
+
if (plurality.isUncertain) {
|
|
40
|
+
badgeClass = 'badge-yellow';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { color: base, badgeClass, opacity };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function pluralityColor(
|
|
48
|
+
plurality: TypeSharedPluralityWire,
|
|
49
|
+
isDark: boolean,
|
|
50
|
+
): string {
|
|
51
|
+
if (plurality.isUncertain) return isDark ? ACCENT_UNCERTAIN_DARK : ACCENT_UNCERTAIN_LIGHT;
|
|
52
|
+
if (plurality.isShared) return isDark ? ACCENT_SHARED_DARK : ACCENT_SHARED_LIGHT;
|
|
53
|
+
return isDark ? BASE_DARK : BASE_LIGHT;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function completenessLabel(completeness: 'complete' | 'partial'): string {
|
|
57
|
+
return completeness === 'partial' ? 'Partial' : 'Complete';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function certaintyLabel(certainty: 'confirmed' | 'possible'): string {
|
|
61
|
+
return certainty === 'possible' ? 'Possible' : 'Confirmed';
|
|
62
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Deterministic hash from string → positive integer (for seeding positions). */
|
|
2
|
+
export function hashSeed(s: string): number {
|
|
3
|
+
let h = 0;
|
|
4
|
+
for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0;
|
|
5
|
+
return Math.abs(h);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Expand-slider → simulation parameters. Maps [0, 10] to physical constants. */
|
|
9
|
+
export function expandParams(v: number): {
|
|
10
|
+
linkDist: number;
|
|
11
|
+
linkStrength: number;
|
|
12
|
+
repMin: number;
|
|
13
|
+
repStrength: number;
|
|
14
|
+
} {
|
|
15
|
+
const t = Math.max(0, Math.min(10, v)) / 10;
|
|
16
|
+
return {
|
|
17
|
+
linkDist: 0.5 + t * 1.45,
|
|
18
|
+
linkStrength: 0.08,
|
|
19
|
+
repMin: 0.55 + t * 0.40,
|
|
20
|
+
repStrength: 0.05,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Which side of a card does an edge connect to? */
|
|
25
|
+
export function portSide(
|
|
26
|
+
from: { x: number; y: number },
|
|
27
|
+
to: { x: number; y: number },
|
|
28
|
+
): 'top' | 'bottom' | 'left' | 'right' {
|
|
29
|
+
const dx = to.x - from.x, dy = to.y - from.y;
|
|
30
|
+
if (Math.abs(dx) > Math.abs(dy)) return dx > 0 ? 'right' : 'left';
|
|
31
|
+
return dy > 0 ? 'bottom' : 'top';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Connection point on a card's side, with offset to prevent stacking. */
|
|
35
|
+
export function portPoint(
|
|
36
|
+
center: { x: number; y: number },
|
|
37
|
+
side: 'top' | 'bottom' | 'left' | 'right',
|
|
38
|
+
w: number,
|
|
39
|
+
h: number,
|
|
40
|
+
offset: number,
|
|
41
|
+
): { x: number; y: number } {
|
|
42
|
+
switch (side) {
|
|
43
|
+
case 'right': return { x: center.x + w / 2, y: center.y + (h / 2 - 6) * offset };
|
|
44
|
+
case 'left': return { x: center.x - w / 2, y: center.y + (h / 2 - 6) * offset };
|
|
45
|
+
case 'bottom': return { x: center.x + (w / 2 - 12) * offset, y: center.y + h / 2 };
|
|
46
|
+
case 'top': return { x: center.x + (w / 2 - 12) * offset, y: center.y - h / 2 };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Reverse lookup: find the node ID whose URI matches, in an ID→URI map. */
|
|
51
|
+
export function idToUriGet(map: Map<string, string>, uri: string): string | undefined {
|
|
52
|
+
for (const [id, u] of map) if (u === uri) return id;
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
@@ -5,6 +5,7 @@ import { useVocabularyStore } from '../stores/vocabulary';
|
|
|
5
5
|
import { useUiStore } from '../stores/ui';
|
|
6
6
|
import { useDsStyle } from '../utils/dataset-style';
|
|
7
7
|
import { useDatasetLoader } from '../composables/use-dataset-loader';
|
|
8
|
+
import { useDatasetStats } from '../composables/use-dataset-stats';
|
|
8
9
|
import { FORMAT_LABELS } from '../config/types';
|
|
9
10
|
import { langName, langLabel, sortLanguages } from '../utils/lang';
|
|
10
11
|
import ConceptCard from '../components/ConceptCard.vue';
|
|
@@ -21,6 +22,7 @@ const store = useVocabularyStore();
|
|
|
21
22
|
const uiStore = useUiStore();
|
|
22
23
|
const { getStyle } = useDsStyle();
|
|
23
24
|
const { ensureLoaded, loading, localError } = useDatasetLoader(() => props.registerId);
|
|
25
|
+
const { stats: datasetStats } = useDatasetStats(() => props.registerId);
|
|
24
26
|
const { t } = useI18n();
|
|
25
27
|
const { localizedDatasetField } = useSiteConfig();
|
|
26
28
|
const route = useRoute();
|
|
@@ -331,10 +333,10 @@ function clearSection() {
|
|
|
331
333
|
<router-link :to="{ name: 'about', params: { registerId } }" class="badge badge-purple hover:opacity-80 transition-opacity">
|
|
332
334
|
{{ t('nav.about') }}
|
|
333
335
|
</router-link>
|
|
334
|
-
<router-link v-if="
|
|
335
|
-
{{
|
|
336
|
+
<router-link v-if="datasetStats?.sourceCount" :to="{ name: 'sources', params: { registerId } }" class="badge badge-gray hover:opacity-80 transition-opacity">
|
|
337
|
+
{{ datasetStats.sourceCount }} {{ t('dataset.sources') }}
|
|
336
338
|
</router-link>
|
|
337
|
-
<span v-if="
|
|
339
|
+
<span v-if="datasetStats?.relationshipCount" class="badge badge-yellow">{{ datasetStats.relationshipCount.toLocaleString() }} {{ t('dataset.relationships') }}</span>
|
|
338
340
|
</div>
|
|
339
341
|
</div>
|
|
340
342
|
|
package/src/views/StatsView.vue
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { computed } from 'vue';
|
|
3
3
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
4
4
|
import { useDsStyle } from '../utils/dataset-style';
|
|
5
5
|
import { useDatasetLoader } from '../composables/use-dataset-loader';
|
|
6
|
+
import { useDatasetStats } from '../composables/use-dataset-stats';
|
|
6
7
|
import { langName, langLabel } from '../utils/lang';
|
|
7
8
|
import { useI18n } from '../i18n';
|
|
8
9
|
|
|
@@ -11,6 +12,7 @@ const props = defineProps<{ registerId?: string }>();
|
|
|
11
12
|
const store = useVocabularyStore();
|
|
12
13
|
const { getColor } = useDsStyle();
|
|
13
14
|
const { loading, localError, ensureLoaded, resolvedId } = useDatasetLoader(() => props.registerId);
|
|
15
|
+
const { stats: statsData, reload: reloadStats } = useDatasetStats(resolvedId);
|
|
14
16
|
const { t } = useI18n();
|
|
15
17
|
|
|
16
18
|
const manifest = computed(() => store.manifests.get(resolvedId.value));
|
|
@@ -54,26 +56,6 @@ function coverageColor(ratio: number): string {
|
|
|
54
56
|
return 'bg-red-400';
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
interface StatsData {
|
|
58
|
-
sourceCount: number;
|
|
59
|
-
sources: Array<{ ref: string; types: string[]; conceptCount: number }>;
|
|
60
|
-
relationshipCount: number;
|
|
61
|
-
relationshipTypes: Record<string, number>;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const statsData = ref<StatsData | null>(null);
|
|
65
|
-
|
|
66
|
-
async function loadStats() {
|
|
67
|
-
if (!resolvedId.value) return;
|
|
68
|
-
const base = import.meta.env.BASE_URL;
|
|
69
|
-
try {
|
|
70
|
-
const resp = await fetch(`${base}data/${resolvedId.value}/stats.json`);
|
|
71
|
-
if (resp.ok) statsData.value = await resp.json();
|
|
72
|
-
} catch {}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
watch(() => resolvedId.value, () => { loadStats(); }, { immediate: true });
|
|
76
|
-
|
|
77
59
|
const relEntries = computed(() => {
|
|
78
60
|
if (!statsData.value?.relationshipTypes) return [];
|
|
79
61
|
return Object.entries(statsData.value.relationshipTypes)
|
|
@@ -92,6 +74,11 @@ function sourceTypeBadge(type: string): string {
|
|
|
92
74
|
if (type === 'lineage') return 'badge-yellow';
|
|
93
75
|
return 'badge-gray';
|
|
94
76
|
}
|
|
77
|
+
|
|
78
|
+
async function reloadAll() {
|
|
79
|
+
await ensureLoaded();
|
|
80
|
+
reloadStats();
|
|
81
|
+
}
|
|
95
82
|
</script>
|
|
96
83
|
|
|
97
84
|
<template>
|
|
@@ -115,7 +102,7 @@ function sourceTypeBadge(type: string): string {
|
|
|
115
102
|
<div class="card p-8 border-red-200 bg-red-50/50 text-center">
|
|
116
103
|
<p class="text-red-700 font-medium mb-1">{{ t('stats.failedToLoad') }}</p>
|
|
117
104
|
<p class="text-sm text-red-600/80 mb-4">{{ localError }}</p>
|
|
118
|
-
<button @click="
|
|
105
|
+
<button @click="reloadAll" class="btn-primary">{{ t('stats.retry') }}</button>
|
|
119
106
|
</div>
|
|
120
107
|
</template>
|
|
121
108
|
<template v-else-if="manifest">
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<section v-if="entries.length" class="bibliography-island">
|
|
3
|
-
<h2 class="text-sm font-semibold text-ink-500 uppercase tracking-wider mb-3">Bibliography</h2>
|
|
4
|
-
<ul class="space-y-1">
|
|
5
|
-
<li v-for="entry in entries" :key="entry.id" class="text-sm flex items-start gap-2">
|
|
6
|
-
<span class="text-ink-400 font-mono text-xs shrink-0">[{{ entry.id }}]</span>
|
|
7
|
-
<div>
|
|
8
|
-
<span class="text-ink-600 dark:text-ink-300">{{ entry.reference }}</span>
|
|
9
|
-
<a v-if="entry.link" :href="entry.link" target="_blank" class="text-blue-600 hover:underline text-xs ml-2">↗</a>
|
|
10
|
-
</div>
|
|
11
|
-
</li>
|
|
12
|
-
</ul>
|
|
13
|
-
</section>
|
|
14
|
-
</template>
|
|
15
|
-
|
|
16
|
-
<script setup lang="ts">
|
|
17
|
-
defineProps<{
|
|
18
|
-
entries: Array<{
|
|
19
|
-
id: string;
|
|
20
|
-
reference: string;
|
|
21
|
-
title?: string;
|
|
22
|
-
link?: string;
|
|
23
|
-
type?: string;
|
|
24
|
-
}>;
|
|
25
|
-
}>();
|
|
26
|
-
</script>
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<!-- Full concept detail using the REAL ConceptDetail.vue -->
|
|
3
|
-
<ErrorBoundary title="Concept view failed" :retry-key="conceptUri">
|
|
4
|
-
<ConceptDetail
|
|
5
|
-
:concept="concept"
|
|
6
|
-
:register-id="registerId"
|
|
7
|
-
:concept-uri="conceptUri"
|
|
8
|
-
/>
|
|
9
|
-
</ErrorBoundary>
|
|
10
|
-
</template>
|
|
11
|
-
|
|
12
|
-
<script setup lang="ts">
|
|
13
|
-
import { onErrorCaptured } from 'vue';
|
|
14
|
-
import ConceptDetail from '../components/ConceptDetail.vue';
|
|
15
|
-
import ErrorBoundary from '../components/ErrorBoundary.vue';
|
|
16
|
-
|
|
17
|
-
defineProps<{
|
|
18
|
-
concept: any;
|
|
19
|
-
registerId: string;
|
|
20
|
-
conceptUri: string;
|
|
21
|
-
}>();
|
|
22
|
-
|
|
23
|
-
onErrorCaptured((err) => {
|
|
24
|
-
console.error('ConceptDetail island error:', err);
|
|
25
|
-
return false;
|
|
26
|
-
});
|
|
27
|
-
</script>
|
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="concept-diff-island">
|
|
3
|
-
<!-- Edition selector -->
|
|
4
|
-
<div v-if="editions.length > 1" class="mb-4">
|
|
5
|
-
<label class="text-sm font-semibold text-ink-500 uppercase tracking-wider mr-2">Compare with:</label>
|
|
6
|
-
<select v-model="compareEdition" class="text-sm border border-ink-200 dark:border-ink-700 rounded px-2 py-1 bg-surface dark:bg-ink-800 text-ink-600 dark:text-ink-300">
|
|
7
|
-
<option value="">Select edition...</option>
|
|
8
|
-
<option v-for="ed in otherEditions" :key="ed.id" :value="ed.id">
|
|
9
|
-
{{ ed.title }} ({{ ed.year ?? '?' }})
|
|
10
|
-
</option>
|
|
11
|
-
</select>
|
|
12
|
-
</div>
|
|
13
|
-
|
|
14
|
-
<!-- Diff rendering -->
|
|
15
|
-
<div v-if="diffResult && compareEdition" class="space-y-4">
|
|
16
|
-
<!-- Similarity score -->
|
|
17
|
-
<div v-if="diffResult.similarity != null" class="flex items-center gap-3">
|
|
18
|
-
<div class="text-sm text-ink-500">Similarity:</div>
|
|
19
|
-
<div class="flex-1 max-w-xs bg-ink-100 dark:bg-ink-800 rounded-full h-3 overflow-hidden">
|
|
20
|
-
<div
|
|
21
|
-
class="h-full rounded-full transition-all duration-300"
|
|
22
|
-
:class="similarityColor(diffResult.similarity)"
|
|
23
|
-
:style="{ width: `${Math.round(diffResult.similarity * 100)}%` }"
|
|
24
|
-
/>
|
|
25
|
-
</div>
|
|
26
|
-
<div class="text-sm font-medium" :class="similarityTextColor(diffResult.similarity)">
|
|
27
|
-
{{ Math.round(diffResult.similarity * 100) }}%
|
|
28
|
-
</div>
|
|
29
|
-
</div>
|
|
30
|
-
|
|
31
|
-
<!-- Per-field diffs -->
|
|
32
|
-
<div v-for="(section, field) in sections" :key="field" class="card p-4">
|
|
33
|
-
<h3 class="text-sm font-semibold text-ink-500 uppercase tracking-wider mb-3">{{ fieldLabel(field) }}</h3>
|
|
34
|
-
<div v-if="section.type === 'added'" class="text-emerald-600 dark:text-emerald-400">
|
|
35
|
-
<span class="text-xs font-medium bg-emerald-100 dark:bg-emerald-900/30 px-1.5 py-0.5 rounded mr-2">ADDED</span>
|
|
36
|
-
<span dir="auto">{{ section.value }}</span>
|
|
37
|
-
</div>
|
|
38
|
-
<div v-else-if="section.type === 'removed'" class="text-red-500 dark:text-red-400 line-through opacity-70">
|
|
39
|
-
<span class="text-xs font-medium bg-red-100 dark:bg-red-900/30 px-1.5 py-0.5 rounded mr-2 no-underline">REMOVED</span>
|
|
40
|
-
<span dir="auto">{{ section.value }}</span>
|
|
41
|
-
</div>
|
|
42
|
-
<div v-else-if="section.type === 'changed'" class="space-y-1">
|
|
43
|
-
<div class="text-red-500 dark:text-red-400 line-through opacity-70" dir="auto">{{ section.oldValue }}</div>
|
|
44
|
-
<div class="text-emerald-600 dark:text-emerald-400" dir="auto">{{ section.newValue }}</div>
|
|
45
|
-
</div>
|
|
46
|
-
<div v-else-if="section.hunks" class="font-mono text-xs space-y-0.5">
|
|
47
|
-
<span
|
|
48
|
-
v-for="(hunk, i) in section.hunks"
|
|
49
|
-
:key="i"
|
|
50
|
-
:class="{
|
|
51
|
-
'bg-emerald-100 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-300': hunk.type === 'added',
|
|
52
|
-
'bg-red-100 dark:bg-red-900/20 text-red-700 dark:text-red-300 line-through': hunk.type === 'removed',
|
|
53
|
-
'text-ink-500': hunk.type === 'equal',
|
|
54
|
-
}"
|
|
55
|
-
dir="auto"
|
|
56
|
-
>{{ hunk.text }}</span>
|
|
57
|
-
</div>
|
|
58
|
-
<div v-else class="text-xs text-ink-400 italic">No changes</div>
|
|
59
|
-
</div>
|
|
60
|
-
</div>
|
|
61
|
-
|
|
62
|
-
<!-- No changes -->
|
|
63
|
-
<div v-else-if="diffResult && diffResult.hasChanges === false" class="text-center py-8">
|
|
64
|
-
<p class="text-ink-400">No differences between these editions.</p>
|
|
65
|
-
</div>
|
|
66
|
-
|
|
67
|
-
<!-- Loading -->
|
|
68
|
-
<div v-else-if="loading" class="text-center py-8">
|
|
69
|
-
<div class="inline-block w-8 h-8 border-4 border-blue-200 border-t-blue-500 rounded-full animate-spin mb-2"></div>
|
|
70
|
-
<p class="text-ink-400 text-sm">Comparing editions...</p>
|
|
71
|
-
</div>
|
|
72
|
-
|
|
73
|
-
<!-- Hint -->
|
|
74
|
-
<div v-else-if="!compareEdition" class="text-center py-4">
|
|
75
|
-
<p class="text-sm text-ink-400">Select an edition above to see what changed.</p>
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
</template>
|
|
79
|
-
|
|
80
|
-
<script setup lang="ts">
|
|
81
|
-
import { ref, computed, watch } from 'vue';
|
|
82
|
-
import { diffConcepts } from 'glossarist/diff';
|
|
83
|
-
import type { ConceptDiff } from 'glossarist/diff';
|
|
84
|
-
|
|
85
|
-
const props = defineProps<{
|
|
86
|
-
currentConcept: any;
|
|
87
|
-
registerId: string;
|
|
88
|
-
editions: Array<{
|
|
89
|
-
id: string;
|
|
90
|
-
title: string;
|
|
91
|
-
year?: number;
|
|
92
|
-
conceptData?: any;
|
|
93
|
-
}>;
|
|
94
|
-
}>();
|
|
95
|
-
|
|
96
|
-
const compareEdition = ref('');
|
|
97
|
-
const diffResult = ref<ConceptDiff | null>(null);
|
|
98
|
-
const loading = ref(false);
|
|
99
|
-
|
|
100
|
-
const otherEditions = computed(() =>
|
|
101
|
-
props.editions.filter(e => e.id !== props.registerId)
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
watch(compareEdition, async (edId) => {
|
|
105
|
-
if (!edId) {
|
|
106
|
-
diffResult.value = null;
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
const edition = props.editions.find(e => e.id === edId);
|
|
110
|
-
if (!edition?.conceptData) {
|
|
111
|
-
diffResult.value = null;
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
loading.value = true;
|
|
115
|
-
try {
|
|
116
|
-
const oldConcept = toConceptLike(edition.conceptData, edId);
|
|
117
|
-
const newConcept = toConceptLike(props.currentConcept, props.registerId);
|
|
118
|
-
diffResult.value = diffConcepts(oldConcept, newConcept);
|
|
119
|
-
} catch (e) {
|
|
120
|
-
console.error('Diff failed:', e);
|
|
121
|
-
diffResult.value = null;
|
|
122
|
-
} finally {
|
|
123
|
-
loading.value = false;
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
function toConceptLike(data: any, registerId: string) {
|
|
128
|
-
const langs = data.languages ?? (data.localizations ? Object.keys(data.localizations) : []);
|
|
129
|
-
return {
|
|
130
|
-
id: data.conceptId ?? data.id,
|
|
131
|
-
termid: String(data.conceptId ?? data.id),
|
|
132
|
-
status: data.status,
|
|
133
|
-
languages: langs,
|
|
134
|
-
localization(lang: string) {
|
|
135
|
-
const loc = (data.localizations ?? {})[lang];
|
|
136
|
-
if (!loc) return null;
|
|
137
|
-
return {
|
|
138
|
-
languageCode: loc.languageCode ?? lang,
|
|
139
|
-
terms: loc.terms ?? [],
|
|
140
|
-
definitions: loc.definitions ?? [],
|
|
141
|
-
notes: loc.notes ?? [],
|
|
142
|
-
examples: loc.examples ?? [],
|
|
143
|
-
};
|
|
144
|
-
},
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const sections = computed(() => {
|
|
149
|
-
if (!diffResult.value) return {};
|
|
150
|
-
const out: Record<string, any> = {};
|
|
151
|
-
const diff = diffResult.value as any;
|
|
152
|
-
const loc = diff.localization?.('eng') ?? diff.localizations?.eng;
|
|
153
|
-
if (!loc) return out;
|
|
154
|
-
|
|
155
|
-
const defChanged = loc.definitions?.changed?.[0];
|
|
156
|
-
if (defChanged?.textDiff?.hunks?.length) {
|
|
157
|
-
out.definition = { hunks: defChanged.textDiff.hunks };
|
|
158
|
-
} else if (loc.definitions?.added?.length) {
|
|
159
|
-
out.definition = { type: 'added', value: loc.definitions.added[0].value?.content ?? loc.definitions.added[0].value };
|
|
160
|
-
} else if (loc.definitions?.removed?.length) {
|
|
161
|
-
out.definition = { type: 'removed', value: loc.definitions.removed[0].value?.content ?? loc.definitions.removed[0].value };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const termChanged = loc.designations?.changed?.[0];
|
|
165
|
-
if (termChanged?.textDiff?.hunks?.length) {
|
|
166
|
-
out.terms = { hunks: termChanged.textDiff.hunks };
|
|
167
|
-
} else if (loc.designations?.added?.length || loc.designations?.removed?.length) {
|
|
168
|
-
out.terms = {
|
|
169
|
-
type: 'changed',
|
|
170
|
-
oldValue: (loc.designations.removed ?? []).map((d: any) => d.value?.designation ?? d.value?.text ?? String(d.value ?? '')).join('; '),
|
|
171
|
-
newValue: (loc.designations.added ?? []).map((d: any) => d.value?.designation ?? d.value?.text ?? String(d.value ?? '')).join('; '),
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (loc.notes?.added?.length || loc.notes?.removed?.length) {
|
|
176
|
-
out.notes = {
|
|
177
|
-
type: 'changed',
|
|
178
|
-
oldValue: (loc.notes.removed ?? []).map((n: any) => n.value?.content ?? n.value?.text ?? String(n.value ?? '')).join('; '),
|
|
179
|
-
newValue: (loc.notes.added ?? []).map((n: any) => n.value?.content ?? n.value?.text ?? String(n.value ?? '')).join('; '),
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (loc.examples?.added?.length || loc.examples?.removed?.length) {
|
|
184
|
-
out.examples = {
|
|
185
|
-
type: 'changed',
|
|
186
|
-
oldValue: (loc.examples.removed ?? []).map((e: any) => e.value?.content ?? e.value?.text ?? String(e.value ?? '')).join('; '),
|
|
187
|
-
newValue: (loc.examples.added ?? []).map((e: any) => e.value?.content ?? e.value?.text ?? String(e.value ?? '')).join('; '),
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return out;
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
function fieldLabel(field: string): string {
|
|
195
|
-
const labels: Record<string, string> = {
|
|
196
|
-
definition: 'Definition',
|
|
197
|
-
terms: 'Terms',
|
|
198
|
-
notes: 'Notes',
|
|
199
|
-
examples: 'Examples',
|
|
200
|
-
};
|
|
201
|
-
return labels[field] ?? field;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function similarityColor(score: number): string {
|
|
205
|
-
if (score >= 0.8) return 'bg-emerald-500';
|
|
206
|
-
if (score >= 0.5) return 'bg-amber-500';
|
|
207
|
-
return 'bg-red-500';
|
|
208
|
-
}
|
|
209
|
-
function similarityTextColor(score: number): string {
|
|
210
|
-
if (score >= 0.8) return 'text-emerald-600 dark:text-emerald-400';
|
|
211
|
-
if (score >= 0.5) return 'text-amber-600 dark:text-amber-400';
|
|
212
|
-
return 'text-red-600 dark:text-red-400';
|
|
213
|
-
}
|
|
214
|
-
</script>
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="concept-island">
|
|
3
|
-
<!-- View mode toggle -->
|
|
4
|
-
<div class="flex items-center justify-between mb-4">
|
|
5
|
-
<div class="flex items-center gap-2">
|
|
6
|
-
<button @click="viewMode = 'detail'" :class="['px-3 py-1 text-sm rounded transition-colors', viewMode === 'detail' ? 'bg-blue-500 text-white' : 'text-ink-500 hover:bg-ink-100 dark:hover:bg-ink-800']">Detail</button>
|
|
7
|
-
<button @click="viewMode = 'sphere'" :class="['px-3 py-1 text-sm rounded transition-colors', viewMode === 'sphere' ? 'bg-blue-500 text-white' : 'text-ink-500 hover:bg-ink-100 dark:hover:bg-ink-800']">Sphere</button>
|
|
8
|
-
</div>
|
|
9
|
-
<button @click="toggleRdf()" class="px-3 py-1 text-sm rounded text-ink-500 hover:bg-ink-100 dark:hover:bg-ink-800 transition-colors">RDF Source {{ rdfOpen ? '▾' : '▸' }}</button>
|
|
10
|
-
</div>
|
|
11
|
-
|
|
12
|
-
<!-- Detail view using REAL SPA components -->
|
|
13
|
-
<div v-if="viewMode === 'detail'" class="space-y-6">
|
|
14
|
-
<!-- Designations from real DesignationList -->
|
|
15
|
-
<DesignationList v-if="hasDesignations" :designations="parsedDesignations" :language="activeLang" />
|
|
16
|
-
|
|
17
|
-
<!-- Definition -->
|
|
18
|
-
<section v-if="concept.definition?.eng">
|
|
19
|
-
<h2 class="text-sm font-semibold text-ink-500 uppercase tracking-wider mb-2">Definition</h2>
|
|
20
|
-
<p class="text-ink-700 dark:text-ink-200 leading-relaxed" dir="auto">{{ concept.definition.eng }}</p>
|
|
21
|
-
</section>
|
|
22
|
-
|
|
23
|
-
<!-- Non-verbal representations from real NonVerbalRepDisplay -->
|
|
24
|
-
<NonVerbalRepDisplay v-if="concept.nonVerbalRep?.length" :reps="concept.nonVerbalRep" :register-id="registerId" />
|
|
25
|
-
|
|
26
|
-
<!-- Format downloads from real FormatDownloads -->
|
|
27
|
-
<FormatDownloads v-if="concept.formats?.length" :formats="concept.formats" :register-id="registerId" />
|
|
28
|
-
|
|
29
|
-
<!-- Groups / Sections -->
|
|
30
|
-
<section v-if="concept.groups?.length">
|
|
31
|
-
<h2 class="text-sm font-semibold text-ink-500 uppercase tracking-wider mb-2">Sections</h2>
|
|
32
|
-
<div class="flex flex-wrap gap-2">
|
|
33
|
-
<span v-for="g in concept.groups" :key="g" class="text-xs px-2 py-1 rounded bg-ink-100 dark:bg-ink-800 text-ink-600 dark:text-ink-300">{{ g }}</span>
|
|
34
|
-
</div>
|
|
35
|
-
</section>
|
|
36
|
-
</div>
|
|
37
|
-
|
|
38
|
-
<!-- Sphere view placeholder -->
|
|
39
|
-
<div v-else class="border border-ink-200 dark:border-ink-700 rounded-lg p-8 text-center">
|
|
40
|
-
<p class="text-ink-400">3D sphere visualization loads on interaction</p>
|
|
41
|
-
</div>
|
|
42
|
-
|
|
43
|
-
<!-- RDF panel — lazy-loaded client-side only (glossarist/rdf is Node-only) -->
|
|
44
|
-
<component v-if="rdfOpen && RdfComponent" :is="RdfComponent" :concept="parsedConcept" :register-id="registerId" :concept-uri-value="conceptUri" />
|
|
45
|
-
</div>
|
|
46
|
-
</template>
|
|
47
|
-
|
|
48
|
-
<script setup lang="ts">
|
|
49
|
-
import { ref, computed, shallowRef, onMounted } from 'vue';
|
|
50
|
-
import DesignationList from '../components/DesignationList.vue';
|
|
51
|
-
import NonVerbalRepDisplay from '../components/NonVerbalRepDisplay.vue';
|
|
52
|
-
import FormatDownloads from '../components/FormatDownloads.vue';
|
|
53
|
-
|
|
54
|
-
const props = defineProps<{
|
|
55
|
-
concept: any;
|
|
56
|
-
registerId: string;
|
|
57
|
-
conceptUri: string;
|
|
58
|
-
}>();
|
|
59
|
-
|
|
60
|
-
const viewMode = ref<'detail' | 'sphere'>('detail');
|
|
61
|
-
const rdfOpen = ref(false);
|
|
62
|
-
const activeLang = ref('eng');
|
|
63
|
-
const RdfComponent = shallowRef<any>(null);
|
|
64
|
-
|
|
65
|
-
// Lazy-load ConceptRdfView only when RDF panel is opened
|
|
66
|
-
// (it imports glossarist/rdf which has Node-only deps — can't SSR)
|
|
67
|
-
async function toggleRdf() {
|
|
68
|
-
rdfOpen.value = !rdfOpen.value;
|
|
69
|
-
if (rdfOpen.value && !RdfComponent.value) {
|
|
70
|
-
try {
|
|
71
|
-
const mod = await import('../components/ConceptRdfView.vue');
|
|
72
|
-
RdfComponent.value = mod.default;
|
|
73
|
-
} catch (e) {
|
|
74
|
-
console.error('Failed to load RDF view:', e);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const hasDesignations = computed(() => {
|
|
80
|
-
return Object.keys(props.concept?.designations || {}).length > 0;
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
const parsedDesignations = computed(() => {
|
|
84
|
-
// Convert the content collection format to what DesignationList expects
|
|
85
|
-
const out: any[] = [];
|
|
86
|
-
for (const [lang, text] of Object.entries(props.concept?.designations || {})) {
|
|
87
|
-
out.push({ designation: text, type: 'expression', normative_status: 'preferred', language_code: lang });
|
|
88
|
-
}
|
|
89
|
-
return out;
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const parsedConcept = computed(() => {
|
|
93
|
-
// Create a minimal Concept-like object for ConceptRdfView
|
|
94
|
-
return {
|
|
95
|
-
id: props.concept?.conceptId,
|
|
96
|
-
uri: props.conceptUri,
|
|
97
|
-
status: props.concept?.status,
|
|
98
|
-
localizations: {},
|
|
99
|
-
...props.concept,
|
|
100
|
-
};
|
|
101
|
-
});
|
|
102
|
-
</script>
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<ErrorBoundary title="Graph view failed" retry-key="graph">
|
|
3
|
-
<GraphPanel :nodes="nodes" :edges="edges" />
|
|
4
|
-
</ErrorBoundary>
|
|
5
|
-
</template>
|
|
6
|
-
|
|
7
|
-
<script setup lang="ts">
|
|
8
|
-
import GraphPanel from '../components/GraphPanel.vue';
|
|
9
|
-
import ErrorBoundary from '../components/ErrorBoundary.vue';
|
|
10
|
-
|
|
11
|
-
defineProps<{
|
|
12
|
-
nodes: Array<{ id: string; label: string; group?: string }>;
|
|
13
|
-
edges: Array<{ source: string; target: string; type?: string }>;
|
|
14
|
-
}>();
|
|
15
|
-
</script>
|