mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +08:00
feat(wiki): honour KB pageType profile in transformations, agent pages & UI
Wiki page classification was only profile-aware in the main ingest pipeline. Transformation outputs hard-coded "synthesis", agent-created pages were left untyped, and the frontend hard-coded the built-in ten types for ordering, colouring and labels — so custom/synthesis types sank to the bottom, rendered grey and showed raw keys. Backend: - Add nullable target_page_type column to mate_wiki_transformation (V142, mysql + h2) plus the entity field and CRUD normalization (blank = use profile fallbackType; membership validated at save time, not edit time). - Route transformation single-run + KB-aggregate page saves through WikiPageTypeProfileService.normalizePageType so output joins the KB classification; agent wiki_create_page now lands on the profile fallbackType instead of an untyped page. Frontend: - Load + parse the KB pageType profile into the wiki store (order, labels, fallbackType) on KB select / refresh. - New useWikiPageType composable: profile-driven label (3-tier fallback) and colour (built-in fixed + deterministic hash palette for custom types). - Sidebar grouping order, graph colouring, node panel, graph filter and the page header badge now follow the profile; transformation editor gains a target-type dropdown sourced from the profile when output target is a page. Refs #292
This commit is contained in:
parent
da5aaba0f7
commit
28f2ba973d
@ -84,6 +84,15 @@ public class WikiTransformationEntity {
|
|||||||
*/
|
*/
|
||||||
private String outputSchema;
|
private String outputSchema;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional target pageType for output that lands as a wiki page
|
||||||
|
* ({@code outputTarget == 'page'}). Normalised against the KB's pageType
|
||||||
|
* profile at save time; {@code null}/blank falls back to the profile's
|
||||||
|
* {@code fallbackType}, so transformation output is always a first-class
|
||||||
|
* member of the KB classification rather than a hard-coded type.
|
||||||
|
*/
|
||||||
|
private String targetPageType;
|
||||||
|
|
||||||
@TableField(fill = FieldFill.INSERT)
|
@TableField(fill = FieldFill.INSERT)
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
|||||||
@ -57,6 +57,12 @@ public class WikiTransformationAggregator {
|
|||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
private WikiEmbeddingService embeddingService;
|
private WikiEmbeddingService embeddingService;
|
||||||
|
|
||||||
|
/** Optional. When wired, the aggregate page is classified against the KB's
|
||||||
|
* pageType profile (template target type, else the profile fallback)
|
||||||
|
* instead of a hard-coded type that sits outside every profile. */
|
||||||
|
@Autowired(required = false)
|
||||||
|
private vip.mate.wiki.profile.WikiPageTypeProfileService pageTypeProfileService;
|
||||||
|
|
||||||
private final com.fasterxml.jackson.databind.ObjectMapper objectMapper =
|
private final com.fasterxml.jackson.databind.ObjectMapper objectMapper =
|
||||||
new com.fasterxml.jackson.databind.ObjectMapper();
|
new com.fasterxml.jackson.databind.ObjectMapper();
|
||||||
|
|
||||||
@ -152,12 +158,16 @@ public class WikiTransformationAggregator {
|
|||||||
+ (triggeredBy == null ? "manual" : triggeredBy);
|
+ (triggeredBy == null ? "manual" : triggeredBy);
|
||||||
String sourceRawIdsJson = toJsonArray(new ArrayList<>(sourceRawIds));
|
String sourceRawIdsJson = toJsonArray(new ArrayList<>(sourceRawIds));
|
||||||
|
|
||||||
|
String pageType = pageTypeProfileService == null
|
||||||
|
? "synthesis"
|
||||||
|
: pageTypeProfileService.normalizePageType(kbId, template.getTargetPageType());
|
||||||
|
|
||||||
WikiPageEntity existing = pageService.getBySlug(kbId, slug);
|
WikiPageEntity existing = pageService.getBySlug(kbId, slug);
|
||||||
WikiPageEntity persisted;
|
WikiPageEntity persisted;
|
||||||
boolean created;
|
boolean created;
|
||||||
if (existing == null) {
|
if (existing == null) {
|
||||||
persisted = pageService.createPage(kbId, slug, title, mergedOutput, summary,
|
persisted = pageService.createPage(kbId, slug, title, mergedOutput, summary,
|
||||||
sourceRawIdsJson, "synthesis");
|
sourceRawIdsJson, pageType);
|
||||||
created = true;
|
created = true;
|
||||||
} else {
|
} else {
|
||||||
persisted = pageService.updatePageByAi(kbId, slug, mergedOutput, summary,
|
persisted = pageService.updatePageByAi(kbId, slug, mergedOutput, summary,
|
||||||
|
|||||||
@ -58,6 +58,12 @@ public class WikiTransformationExecutor {
|
|||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
private WikiPageService pageService;
|
private WikiPageService pageService;
|
||||||
|
|
||||||
|
/** Optional. When wired, a run saved as a page is classified against the
|
||||||
|
* KB's pageType profile (template target type, else the profile fallback)
|
||||||
|
* instead of a hard-coded type that sits outside every profile. */
|
||||||
|
@Autowired(required = false)
|
||||||
|
private vip.mate.wiki.profile.WikiPageTypeProfileService pageTypeProfileService;
|
||||||
|
|
||||||
/** Optional. When wired, every persisted synthesis page is embedded so
|
/** Optional. When wired, every persisted synthesis page is embedded so
|
||||||
* the semantic retriever can surface it on terms that exist only in the
|
* the semantic retriever can surface it on terms that exist only in the
|
||||||
* transformation output (not in any source raw's chunks). */
|
* transformation output (not in any source raw's chunks). */
|
||||||
@ -628,12 +634,13 @@ public class WikiTransformationExecutor {
|
|||||||
String title = template.getTitle() + " · " + safeTitle(raw);
|
String title = template.getTitle() + " · " + safeTitle(raw);
|
||||||
String summary = deriveSummary(output);
|
String summary = deriveSummary(output);
|
||||||
String sourceRawIdsJson = toJsonArray(raw.getId());
|
String sourceRawIdsJson = toJsonArray(raw.getId());
|
||||||
|
String pageType = resolvePageType(kbId, template);
|
||||||
|
|
||||||
WikiPageEntity existing = pageService.getBySlug(kbId, slug);
|
WikiPageEntity existing = pageService.getBySlug(kbId, slug);
|
||||||
WikiPageEntity persisted;
|
WikiPageEntity persisted;
|
||||||
if (existing == null) {
|
if (existing == null) {
|
||||||
persisted = pageService.createPage(kbId, slug, title, output, summary,
|
persisted = pageService.createPage(kbId, slug, title, output, summary,
|
||||||
sourceRawIdsJson, "synthesis");
|
sourceRawIdsJson, pageType);
|
||||||
log.info("[WikiTransformation] saved run={} as new page slug={} pageId={}",
|
log.info("[WikiTransformation] saved run={} as new page slug={} pageId={}",
|
||||||
run.getId(), slug, persisted.getId());
|
run.getId(), slug, persisted.getId());
|
||||||
} else {
|
} else {
|
||||||
@ -686,6 +693,19 @@ public class WikiTransformationExecutor {
|
|||||||
"\\.(pdf|docx?|pptx?|xlsx?|csv|tsv|txt|md|markdown|rtf|odt|epub|html?|json|xml|yaml|yml|jpe?g|png|gif|bmp|tiff?|webp|svg|mp3|wav|mp4|mov|webm)$",
|
"\\.(pdf|docx?|pptx?|xlsx?|csv|tsv|txt|md|markdown|rtf|odt|epub|html?|json|xml|yaml|yml|jpe?g|png|gif|bmp|tiff?|webp|svg|mp3|wav|mp4|mov|webm)$",
|
||||||
java.util.regex.Pattern.CASE_INSENSITIVE);
|
java.util.regex.Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classify the saved page against the KB's pageType profile: use the
|
||||||
|
* template's declared target type, normalised (an unknown / blank type is
|
||||||
|
* downgraded to the profile's fallbackType). Falls back to the legacy
|
||||||
|
* {@code "synthesis"} only when the profile service is not wired.
|
||||||
|
*/
|
||||||
|
private String resolvePageType(Long kbId, WikiTransformationEntity template) {
|
||||||
|
if (pageTypeProfileService == null) {
|
||||||
|
return "synthesis";
|
||||||
|
}
|
||||||
|
return pageTypeProfileService.normalizePageType(kbId, template.getTargetPageType());
|
||||||
|
}
|
||||||
|
|
||||||
private static String buildSlug(WikiTransformationEntity template, WikiRawMaterialEntity raw) {
|
private static String buildSlug(WikiTransformationEntity template, WikiRawMaterialEntity raw) {
|
||||||
String trimmedTitle = stripFileExtension(raw.getTitle());
|
String trimmedTitle = stripFileExtension(raw.getTitle());
|
||||||
String rawPart = WikiPageService.toSlug(trimmedTitle);
|
String rawPart = WikiPageService.toSlug(trimmedTitle);
|
||||||
|
|||||||
@ -112,6 +112,7 @@ public class WikiTransformationService {
|
|||||||
entity.setOutputTarget(normalizeOutputTarget(input.getOutputTarget()));
|
entity.setOutputTarget(normalizeOutputTarget(input.getOutputTarget()));
|
||||||
entity.setOutputFormat(normalizeOutputFormat(input.getOutputFormat()));
|
entity.setOutputFormat(normalizeOutputFormat(input.getOutputFormat()));
|
||||||
entity.setOutputSchema(sanitizeOutputSchema(input.getOutputSchema()));
|
entity.setOutputSchema(sanitizeOutputSchema(input.getOutputSchema()));
|
||||||
|
entity.setTargetPageType(normalizeTargetPageType(input.getTargetPageType()));
|
||||||
transformationMapper.insert(entity);
|
transformationMapper.insert(entity);
|
||||||
log.info("[WikiTransformation] created id={} name={} kbId={}",
|
log.info("[WikiTransformation] created id={} name={} kbId={}",
|
||||||
entity.getId(), entity.getName(), entity.getKbId());
|
entity.getId(), entity.getName(), entity.getKbId());
|
||||||
@ -144,6 +145,10 @@ public class WikiTransformationService {
|
|||||||
// Empty string clears the schema; non-blank gets stored after a parse check.
|
// Empty string clears the schema; non-blank gets stored after a parse check.
|
||||||
entity.setOutputSchema(sanitizeOutputSchema(patch.getOutputSchema()));
|
entity.setOutputSchema(sanitizeOutputSchema(patch.getOutputSchema()));
|
||||||
}
|
}
|
||||||
|
if (patch.getTargetPageType() != null) {
|
||||||
|
// Empty string clears (back to profile fallback); non-blank is stored lowercase.
|
||||||
|
entity.setTargetPageType(normalizeTargetPageType(patch.getTargetPageType()));
|
||||||
|
}
|
||||||
transformationMapper.updateById(entity);
|
transformationMapper.updateById(entity);
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
@ -158,6 +163,19 @@ public class WikiTransformationService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalise the optional target pageType. Blank / null means "auto" —
|
||||||
|
* stored as {@code null} so the executor falls back to the profile's
|
||||||
|
* {@code fallbackType} at save time. Membership against the KB profile is
|
||||||
|
* NOT validated here: that is deferred to {@code normalizePageType} at
|
||||||
|
* page-save time, so editing a profile never breaks an existing template.
|
||||||
|
*/
|
||||||
|
private static String normalizeTargetPageType(String raw) {
|
||||||
|
if (raw == null) return null;
|
||||||
|
String trimmed = raw.trim();
|
||||||
|
return trimmed.isEmpty() ? null : trimmed.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
/** Whitelist incoming outputFormat; unknown / null = "markdown". */
|
/** Whitelist incoming outputFormat; unknown / null = "markdown". */
|
||||||
private static String normalizeOutputFormat(String raw) {
|
private static String normalizeOutputFormat(String raw) {
|
||||||
if (raw == null) return "markdown";
|
if (raw == null) return "markdown";
|
||||||
|
|||||||
@ -91,6 +91,12 @@ public class WikiTool {
|
|||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
private ApprovalWorkflowService approvalWorkflowService;
|
private ApprovalWorkflowService approvalWorkflowService;
|
||||||
|
|
||||||
|
/** Optional. Classifies an agent-authored page against the KB's pageType
|
||||||
|
* profile fallback so it lands inside the KB classification rather than
|
||||||
|
* as an untyped page. Absent in lightweight contexts — page stays untyped. */
|
||||||
|
@Autowired(required = false)
|
||||||
|
private vip.mate.wiki.profile.WikiPageTypeProfileService pageTypeProfileService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Per-agent pageType permission gate. Mandatory: this is a security control,
|
* Per-agent pageType permission gate. Mandatory: this is a security control,
|
||||||
* so it is a required constructor dependency rather than an optional bean —
|
* so it is a required constructor dependency rather than an optional bean —
|
||||||
@ -489,8 +495,13 @@ public class WikiTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String summary = content.length() > 200 ? content.substring(0, 200) + "..." : content;
|
String summary = content.length() > 200 ? content.substring(0, 200) + "..." : content;
|
||||||
WikiPageEntity page = pageService.createPage(kbId, slug, title, content, summary, null);
|
// Classify into the KB profile's fallbackType so an agent-authored page
|
||||||
log.info("[WikiTool] Created page: {} (slug={}, kbId={})", title, slug, kbId);
|
// joins the KB classification instead of being stored untyped.
|
||||||
|
String pageType = pageTypeProfileService == null
|
||||||
|
? null
|
||||||
|
: pageTypeProfileService.normalizePageType(kbId, null);
|
||||||
|
WikiPageEntity page = pageService.createPage(kbId, slug, title, content, summary, null, pageType);
|
||||||
|
log.info("[WikiTool] Created page: {} (slug={}, kbId={}, type={})", title, slug, kbId, pageType);
|
||||||
|
|
||||||
return JSONUtil.createObj()
|
return JSONUtil.createObj()
|
||||||
.set("ok", true)
|
.set("ok", true)
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
-- Optional target pageType for a transformation whose output_target='page'.
|
||||||
|
-- When set, a run persisted as a wiki page is classified with this pageType
|
||||||
|
-- (normalised against the KB's pageType profile at save time). When NULL the
|
||||||
|
-- save falls back to the profile's fallbackType, so transformation output is
|
||||||
|
-- always a first-class member of the KB's classification rather than a
|
||||||
|
-- hard-coded "synthesis" type that sits outside every profile.
|
||||||
|
|
||||||
|
ALTER TABLE mate_wiki_transformation
|
||||||
|
ADD COLUMN IF NOT EXISTS target_page_type VARCHAR(64) DEFAULT NULL;
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
-- Optional target pageType for a transformation whose output_target='page'.
|
||||||
|
-- See the h2 sibling migration for the prose explanation. MySQL lacks
|
||||||
|
-- `ADD COLUMN IF NOT EXISTS`, so the column is guarded by an
|
||||||
|
-- INFORMATION_SCHEMA check + prepared statement.
|
||||||
|
|
||||||
|
SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'mate_wiki_transformation'
|
||||||
|
AND COLUMN_NAME = 'target_page_type');
|
||||||
|
SET @s := IF(@c = 0,
|
||||||
|
'ALTER TABLE mate_wiki_transformation ADD COLUMN target_page_type VARCHAR(64) DEFAULT NULL',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
@ -879,6 +879,7 @@ export const wikiApi = {
|
|||||||
outputTarget?: 'none' | 'page'
|
outputTarget?: 'none' | 'page'
|
||||||
outputFormat?: 'markdown' | 'json'
|
outputFormat?: 'markdown' | 'json'
|
||||||
outputSchema?: string | null
|
outputSchema?: string | null
|
||||||
|
targetPageType?: string | null
|
||||||
}) =>
|
}) =>
|
||||||
http.post('/wiki/transformations', data),
|
http.post('/wiki/transformations', data),
|
||||||
updateTransformation: (id: number, data: {
|
updateTransformation: (id: number, data: {
|
||||||
@ -891,6 +892,7 @@ export const wikiApi = {
|
|||||||
outputTarget?: 'none' | 'page'
|
outputTarget?: 'none' | 'page'
|
||||||
outputFormat?: 'markdown' | 'json'
|
outputFormat?: 'markdown' | 'json'
|
||||||
outputSchema?: string | null
|
outputSchema?: string | null
|
||||||
|
targetPageType?: string | null
|
||||||
}) =>
|
}) =>
|
||||||
http.put(`/wiki/transformations/${id}`, data),
|
http.put(`/wiki/transformations/${id}`, data),
|
||||||
deleteTransformation: (id: number) =>
|
deleteTransformation: (id: number) =>
|
||||||
|
|||||||
69
mateclaw-ui/src/composables/useWikiPageType.ts
Normal file
69
mateclaw-ui/src/composables/useWikiPageType.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useWikiStore } from '@/stores/useWikiStore'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared pageType presentation helpers, driven by the active KB's pageType
|
||||||
|
* profile (loaded into the wiki store). Centralises label resolution and
|
||||||
|
* colouring so the sidebar, graph view, node panel and toolbar all render a
|
||||||
|
* KB's custom classification consistently instead of each hard-coding the
|
||||||
|
* built-in ten types.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Fixed colours for the built-in ten types so existing KBs look unchanged.
|
||||||
|
const BUILTIN_COLORS: Record<string, string> = {
|
||||||
|
concept: '#D96E46',
|
||||||
|
person: '#5B8DEF',
|
||||||
|
place: '#4CAF82',
|
||||||
|
event: '#F59E0B',
|
||||||
|
technology: '#8B5CF6',
|
||||||
|
organization: '#EC4899',
|
||||||
|
product: '#14B8A6',
|
||||||
|
term: '#6B7280',
|
||||||
|
process: '#F97316',
|
||||||
|
other: '#9CA3AF',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Palette for custom / synthesis types not in the built-in map. A type name is
|
||||||
|
// hashed to a stable index so the same type always gets the same colour within
|
||||||
|
// and across sessions, without needing a colour field on the profile schema.
|
||||||
|
const HASH_PALETTE = [
|
||||||
|
'#0EA5E9', '#A855F7', '#22C55E', '#EAB308', '#EF4444',
|
||||||
|
'#06B6D4', '#D946EF', '#84CC16', '#F43F5E', '#3B82F6',
|
||||||
|
'#10B981', '#FB923C',
|
||||||
|
]
|
||||||
|
|
||||||
|
function hashIndex(s: string, mod: number): number {
|
||||||
|
let h = 0
|
||||||
|
for (let i = 0; i < s.length; i++) {
|
||||||
|
h = (h * 31 + s.charCodeAt(i)) | 0
|
||||||
|
}
|
||||||
|
return Math.abs(h) % mod
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWikiPageType() {
|
||||||
|
const store = useWikiStore()
|
||||||
|
const { t, te } = useI18n()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display label for a pageType, three-tier fallback:
|
||||||
|
* profile label → i18n `wiki.pageTypes.{type}` → capitalised raw key.
|
||||||
|
*/
|
||||||
|
function formatPageTypeLabel(type: string | null | undefined): string {
|
||||||
|
const key = (type || '').trim().toLowerCase()
|
||||||
|
if (!key) return ''
|
||||||
|
const fromProfile = store.pageTypeProfile?.labels?.[key]
|
||||||
|
if (fromProfile) return fromProfile
|
||||||
|
const i18nKey = `wiki.pageTypes.${key}`
|
||||||
|
if (te(i18nKey)) return t(i18nKey)
|
||||||
|
return key.charAt(0).toUpperCase() + key.slice(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stable colour for a pageType: built-in fixed colour, else hashed palette. */
|
||||||
|
function typeColor(type: string | null | undefined): string {
|
||||||
|
const key = (type || 'other').toLowerCase()
|
||||||
|
if (BUILTIN_COLORS[key]) return BUILTIN_COLORS[key]
|
||||||
|
return HASH_PALETTE[hashIndex(key, HASH_PALETTE.length)]
|
||||||
|
}
|
||||||
|
|
||||||
|
return { formatPageTypeLabel, typeColor }
|
||||||
|
}
|
||||||
@ -2115,6 +2115,9 @@ export default {
|
|||||||
outputSchemaHelp: 'Schema text is injected into the prompt and checked for required fields after parsing. Leave blank to skip validation.',
|
outputSchemaHelp: 'Schema text is injected into the prompt and checked for required fields after parsing. Leave blank to skip validation.',
|
||||||
outputSchemaPlaceholder: '{\n "type": "object",\n "required": ["title", "items"],\n "properties": {\n "title": { "type": "string" },\n "items": { "type": "array" }\n }\n}',
|
outputSchemaPlaceholder: '{\n "type": "object",\n "required": ["title", "items"],\n "properties": {\n "title": { "type": "string" },\n "items": { "type": "array" }\n }\n}',
|
||||||
outputSchemaBadge: 'Schema',
|
outputSchemaBadge: 'Schema',
|
||||||
|
targetPageType: 'Target type',
|
||||||
|
targetPageTypeAuto: '(Default / auto)',
|
||||||
|
targetPageTypeHelp: "The page type used when this output lands as a page, drawn from this knowledge base's pageType profile. Leave blank to use the profile's fallback type.",
|
||||||
saveAsPageBtn: 'Save as page',
|
saveAsPageBtn: 'Save as page',
|
||||||
saving: 'Saving…',
|
saving: 'Saving…',
|
||||||
savedAsPage: 'Saved as:',
|
savedAsPage: 'Saved as:',
|
||||||
|
|||||||
@ -2127,6 +2127,9 @@ export default {
|
|||||||
outputSchemaHelp: '写在这里的 JSON Schema 会注入到 prompt,并在解析后做必填字段校验。留空表示不校验。',
|
outputSchemaHelp: '写在这里的 JSON Schema 会注入到 prompt,并在解析后做必填字段校验。留空表示不校验。',
|
||||||
outputSchemaPlaceholder: '{\n "type": "object",\n "required": ["title", "items"],\n "properties": {\n "title": { "type": "string" },\n "items": { "type": "array" }\n }\n}',
|
outputSchemaPlaceholder: '{\n "type": "object",\n "required": ["title", "items"],\n "properties": {\n "title": { "type": "string" },\n "items": { "type": "array" }\n }\n}',
|
||||||
outputSchemaBadge: 'Schema',
|
outputSchemaBadge: 'Schema',
|
||||||
|
targetPageType: '目标分类',
|
||||||
|
targetPageTypeAuto: '(默认 / 自动)',
|
||||||
|
targetPageTypeHelp: '落页时使用的页面分类,来自当前知识库的分类配置;留空则使用配置中的兜底分类。',
|
||||||
saveAsPageBtn: '保存为页面',
|
saveAsPageBtn: '保存为页面',
|
||||||
saving: '保存中…',
|
saving: '保存中…',
|
||||||
savedAsPage: '已保存:',
|
savedAsPage: '已保存:',
|
||||||
|
|||||||
@ -76,6 +76,20 @@ export interface WikiPageRef {
|
|||||||
archived: boolean
|
archived: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parsed view of a KB's pageType profile, derived from the GET
|
||||||
|
* page-type-profile endpoint's `config` JSON. `order` preserves the profile's
|
||||||
|
* declared pageType ordering (the JSON object key order); `labels` maps each
|
||||||
|
* pageType to its display label. Consumed by the sidebar grouping, graph
|
||||||
|
* colouring and the transformation editor's target-type dropdown so all of
|
||||||
|
* them follow the KB's own classification instead of a hard-coded list.
|
||||||
|
*/
|
||||||
|
export interface WikiPageTypeProfile {
|
||||||
|
fallbackType: string
|
||||||
|
order: string[]
|
||||||
|
labels: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
/** Per-page row in a broken-links report. */
|
/** Per-page row in a broken-links report. */
|
||||||
export interface WikiBrokenLinkPage {
|
export interface WikiBrokenLinkPage {
|
||||||
// Snowflake — stay as string end-to-end.
|
// Snowflake — stay as string end-to-end.
|
||||||
@ -121,6 +135,11 @@ export const useWikiStore = defineStore('wiki', () => {
|
|||||||
const selectedRawId = ref<number | null>(null)
|
const selectedRawId = ref<number | null>(null)
|
||||||
const totalPageCount = ref(0)
|
const totalPageCount = ref(0)
|
||||||
|
|
||||||
|
// Active KB's pageType profile (parsed). Loaded alongside the KB so sidebar
|
||||||
|
// grouping, graph colouring and the transformation editor can render the
|
||||||
|
// KB's own classification. Null until a KB is selected / its profile loads.
|
||||||
|
const pageTypeProfile = ref<WikiPageTypeProfile | null>(null)
|
||||||
|
|
||||||
// Wikilink resolution index — kept separate from `pages` because (a) it must
|
// Wikilink resolution index — kept separate from `pages` because (a) it must
|
||||||
// survive the raw-material filter, and (b) the viewer's postprocess needs an
|
// survive the raw-material filter, and (b) the viewer's postprocess needs an
|
||||||
// O(1) slug/title lookup over the full KB. `archivedPageRefs` is only loaded
|
// O(1) slug/title lookup over the full KB. `archivedPageRefs` is only loaded
|
||||||
@ -169,9 +188,40 @@ export const useWikiStore = defineStore('wiki', () => {
|
|||||||
fetchPages(id),
|
fetchPages(id),
|
||||||
fetchPageRefs(id),
|
fetchPageRefs(id),
|
||||||
loadBrokenLinksReport(id),
|
loadBrokenLinksReport(id),
|
||||||
|
loadPageTypeProfile(id),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load + parse the KB's pageType profile into {@link pageTypeProfile}. The
|
||||||
|
* endpoint returns `config` as a JSON string (built-in default when the KB
|
||||||
|
* has no custom profile); we extract the declared type order, labels and
|
||||||
|
* fallbackType. Failures degrade to a null profile — consumers then fall
|
||||||
|
* back to i18n labels / hash colouring rather than breaking the view.
|
||||||
|
*/
|
||||||
|
async function loadPageTypeProfile(kbId: number) {
|
||||||
|
try {
|
||||||
|
const res: any = await wikiApi.getPageTypeProfile(kbId)
|
||||||
|
const payload = res.data ?? res
|
||||||
|
const cfg = JSON.parse(payload.config || '{}')
|
||||||
|
const types = cfg.pageTypes && typeof cfg.pageTypes === 'object' ? cfg.pageTypes : {}
|
||||||
|
const order = Object.keys(types)
|
||||||
|
const labels: Record<string, string> = {}
|
||||||
|
for (const key of order) {
|
||||||
|
const def = types[key]
|
||||||
|
labels[key] = (def && typeof def.label === 'string' && def.label) || key
|
||||||
|
}
|
||||||
|
pageTypeProfile.value = {
|
||||||
|
fallbackType: typeof cfg.fallbackType === 'string' ? cfg.fallbackType : 'concept',
|
||||||
|
order,
|
||||||
|
labels,
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Wiki] Failed to load pageType profile', e)
|
||||||
|
pageTypeProfile.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function createKB(data: { name: string; description?: string; agentId?: number }) {
|
async function createKB(data: { name: string; description?: string; agentId?: number }) {
|
||||||
const res: any = await wikiApi.createKB(data)
|
const res: any = await wikiApi.createKB(data)
|
||||||
const kb = res.data || res
|
const kb = res.data || res
|
||||||
@ -201,6 +251,7 @@ export const useWikiStore = defineStore('wiki', () => {
|
|||||||
if (brokenLinksPollTimer) { clearInterval(brokenLinksPollTimer); brokenLinksPollTimer = null }
|
if (brokenLinksPollTimer) { clearInterval(brokenLinksPollTimer); brokenLinksPollTimer = null }
|
||||||
brokenLinksLoading.value = false
|
brokenLinksLoading.value = false
|
||||||
selectedRawId.value = null
|
selectedRawId.value = null
|
||||||
|
pageTypeProfile.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchRawMaterials(kbId: number) {
|
async function fetchRawMaterials(kbId: number) {
|
||||||
@ -326,6 +377,7 @@ export const useWikiStore = defineStore('wiki', () => {
|
|||||||
// Keep refs in lockstep with the rest of the KB state so a freshly
|
// Keep refs in lockstep with the rest of the KB state so a freshly
|
||||||
// created page is immediately resolvable by the viewer.
|
// created page is immediately resolvable by the viewer.
|
||||||
fetchPageRefs(kbId),
|
fetchPageRefs(kbId),
|
||||||
|
loadPageTypeProfile(kbId),
|
||||||
])
|
])
|
||||||
const nextKB = (kbRes as any).data || kbRes
|
const nextKB = (kbRes as any).data || kbRes
|
||||||
currentKB.value = nextKB
|
currentKB.value = nextKB
|
||||||
@ -394,6 +446,7 @@ export const useWikiStore = defineStore('wiki', () => {
|
|||||||
loading,
|
loading,
|
||||||
selectedRawId,
|
selectedRawId,
|
||||||
totalPageCount,
|
totalPageCount,
|
||||||
|
pageTypeProfile,
|
||||||
pageRefs,
|
pageRefs,
|
||||||
archivedPageRefs,
|
archivedPageRefs,
|
||||||
brokenLinksReport,
|
brokenLinksReport,
|
||||||
@ -410,6 +463,7 @@ export const useWikiStore = defineStore('wiki', () => {
|
|||||||
fetchArchivedPageRefs,
|
fetchArchivedPageRefs,
|
||||||
loadBrokenLinksReport,
|
loadBrokenLinksReport,
|
||||||
startBrokenLinksScan,
|
startBrokenLinksScan,
|
||||||
|
loadPageTypeProfile,
|
||||||
refreshCurrentKB,
|
refreshCurrentKB,
|
||||||
filterPagesByRaw,
|
filterPagesByRaw,
|
||||||
clearRawFilter,
|
clearRawFilter,
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<div class="page-header-left">
|
<div class="page-header-left">
|
||||||
<!-- Page type badge -->
|
<!-- Page type badge -->
|
||||||
<span v-if="page.pageType" class="page-type-badge" :class="page.pageType">
|
<span v-if="page.pageType" class="page-type-badge" :class="page.pageType">
|
||||||
{{ t(`wiki.page.type.${page.pageType}`) || page.pageType }}
|
{{ formatPageTypeLabel(page.pageType) }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<h2 class="page-title">{{ page.title }}</h2>
|
<h2 class="page-title">{{ page.title }}</h2>
|
||||||
@ -42,8 +42,10 @@ import { computed } from 'vue'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { Link } from '@element-plus/icons-vue'
|
import { Link } from '@element-plus/icons-vue'
|
||||||
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
|
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
|
||||||
|
import { useWikiPageType } from '@/composables/useWikiPageType'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { formatPageTypeLabel } = useWikiPageType()
|
||||||
const workspace = useWorkspaceStore()
|
const workspace = useWorkspaceStore()
|
||||||
|
|
||||||
// Enriching a page (adding cross-links) is a write action — viewers only read.
|
// Enriching a page (adding cross-links) is a write action — viewers only read.
|
||||||
|
|||||||
@ -262,6 +262,17 @@
|
|||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<label v-if="form.outputTarget === 'page'" class="field">
|
||||||
|
<span class="field-label">{{ t('wiki.transformations.targetPageType') }}</span>
|
||||||
|
<select v-model="form.targetPageType" class="field-input">
|
||||||
|
<option value="">{{ t('wiki.transformations.targetPageTypeAuto') }}</option>
|
||||||
|
<option v-for="opt in pageTypeOptions" :key="opt.value" :value="opt.value">
|
||||||
|
{{ opt.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<span class="field-hint">{{ t('wiki.transformations.targetPageTypeHelp') }}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
<fieldset class="field field--group">
|
<fieldset class="field field--group">
|
||||||
<legend class="field-label">{{ t('wiki.transformations.outputFormatLabel') }}</legend>
|
<legend class="field-label">{{ t('wiki.transformations.outputFormatLabel') }}</legend>
|
||||||
<label class="radio-row">
|
<label class="radio-row">
|
||||||
@ -353,6 +364,7 @@ interface WikiTransformation {
|
|||||||
outputTarget: 'none' | 'page' | null
|
outputTarget: 'none' | 'page' | null
|
||||||
outputFormat: 'markdown' | 'json' | null
|
outputFormat: 'markdown' | 'json' | null
|
||||||
outputSchema: string | null
|
outputSchema: string | null
|
||||||
|
targetPageType: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WikiTransformationRun {
|
interface WikiTransformationRun {
|
||||||
@ -411,6 +423,7 @@ const form = reactive<{
|
|||||||
outputTarget: 'none' | 'page'
|
outputTarget: 'none' | 'page'
|
||||||
outputFormat: 'markdown' | 'json'
|
outputFormat: 'markdown' | 'json'
|
||||||
outputSchema: string
|
outputSchema: string
|
||||||
|
targetPageType: string
|
||||||
modelId: number | null
|
modelId: number | null
|
||||||
}>({
|
}>({
|
||||||
name: '',
|
name: '',
|
||||||
@ -422,9 +435,20 @@ const form = reactive<{
|
|||||||
outputTarget: 'none',
|
outputTarget: 'none',
|
||||||
outputFormat: 'markdown',
|
outputFormat: 'markdown',
|
||||||
outputSchema: '',
|
outputSchema: '',
|
||||||
|
targetPageType: '',
|
||||||
modelId: null,
|
modelId: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Target-type dropdown options come from the active KB's pageType profile.
|
||||||
|
const pageTypeOptions = computed(() => {
|
||||||
|
const profile = store.pageTypeProfile
|
||||||
|
if (!profile) return [] as { value: string; label: string }[]
|
||||||
|
return profile.order.map((type) => ({
|
||||||
|
value: type,
|
||||||
|
label: profile.labels[type] || type,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
const completedRaws = computed<WikiRawMaterial[]>(() =>
|
const completedRaws = computed<WikiRawMaterial[]>(() =>
|
||||||
store.rawMaterials.filter(
|
store.rawMaterials.filter(
|
||||||
(r) => r.processingStatus === 'completed' || r.processingStatus === 'partial'
|
(r) => r.processingStatus === 'completed' || r.processingStatus === 'partial'
|
||||||
@ -517,6 +541,7 @@ function openCreate() {
|
|||||||
form.outputTarget = 'none'
|
form.outputTarget = 'none'
|
||||||
form.outputFormat = 'markdown'
|
form.outputFormat = 'markdown'
|
||||||
form.outputSchema = ''
|
form.outputSchema = ''
|
||||||
|
form.targetPageType = ''
|
||||||
form.modelId = null
|
form.modelId = null
|
||||||
editorOpen.value = true
|
editorOpen.value = true
|
||||||
ensureModelsLoaded()
|
ensureModelsLoaded()
|
||||||
@ -533,6 +558,7 @@ function openEdit(tpl: WikiTransformation) {
|
|||||||
form.outputTarget = tpl.outputTarget === 'page' ? 'page' : 'none'
|
form.outputTarget = tpl.outputTarget === 'page' ? 'page' : 'none'
|
||||||
form.outputFormat = tpl.outputFormat === 'json' ? 'json' : 'markdown'
|
form.outputFormat = tpl.outputFormat === 'json' ? 'json' : 'markdown'
|
||||||
form.outputSchema = tpl.outputSchema || ''
|
form.outputSchema = tpl.outputSchema || ''
|
||||||
|
form.targetPageType = tpl.targetPageType || ''
|
||||||
form.modelId = tpl.modelId ?? null
|
form.modelId = tpl.modelId ?? null
|
||||||
editorOpen.value = true
|
editorOpen.value = true
|
||||||
ensureModelsLoaded()
|
ensureModelsLoaded()
|
||||||
@ -554,6 +580,9 @@ async function onSave() {
|
|||||||
// Schema is only persisted when format=json; otherwise we always send
|
// Schema is only persisted when format=json; otherwise we always send
|
||||||
// an empty string so the backend can clear a previously-stored value.
|
// an empty string so the backend can clear a previously-stored value.
|
||||||
const schemaPayload = form.outputFormat === 'json' ? form.outputSchema.trim() : ''
|
const schemaPayload = form.outputFormat === 'json' ? form.outputSchema.trim() : ''
|
||||||
|
// Target pageType only applies when output lands as a page; otherwise send
|
||||||
|
// an empty string so the backend clears any previously-stored value.
|
||||||
|
const targetPageTypePayload = form.outputTarget === 'page' ? form.targetPageType : ''
|
||||||
if (editing.value) {
|
if (editing.value) {
|
||||||
// Update path: backend treats `-1` as "clear modelId"; null is skipped.
|
// Update path: backend treats `-1` as "clear modelId"; null is skipped.
|
||||||
const updateModelId = form.modelId == null ? -1 : form.modelId
|
const updateModelId = form.modelId == null ? -1 : form.modelId
|
||||||
@ -566,6 +595,7 @@ async function onSave() {
|
|||||||
outputTarget: form.outputTarget,
|
outputTarget: form.outputTarget,
|
||||||
outputFormat: form.outputFormat,
|
outputFormat: form.outputFormat,
|
||||||
outputSchema: schemaPayload,
|
outputSchema: schemaPayload,
|
||||||
|
targetPageType: targetPageTypePayload,
|
||||||
modelId: updateModelId,
|
modelId: updateModelId,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@ -580,6 +610,7 @@ async function onSave() {
|
|||||||
outputTarget: form.outputTarget,
|
outputTarget: form.outputTarget,
|
||||||
outputFormat: form.outputFormat,
|
outputFormat: form.outputFormat,
|
||||||
outputSchema: schemaPayload || null,
|
outputSchema: schemaPayload || null,
|
||||||
|
targetPageType: targetPageTypePayload || null,
|
||||||
modelId: form.modelId,
|
modelId: form.modelId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<div class="node-panel">
|
<div class="node-panel">
|
||||||
<div class="node-panel-header">
|
<div class="node-panel-header">
|
||||||
<span class="node-type-badge" :style="{ background: typeColor(page.pageType) }">
|
<span class="node-type-badge" :style="{ background: typeColor(page.pageType) }">
|
||||||
{{ t(`wiki.pageTypes.${page.pageType || 'other'}`, page.pageType || 'other') }}
|
{{ formatPageTypeLabel(page.pageType || 'other') }}
|
||||||
</span>
|
</span>
|
||||||
<button class="node-panel-close" @click="emit('close')">✕</button>
|
<button class="node-panel-close" @click="emit('close')">✕</button>
|
||||||
</div>
|
</div>
|
||||||
@ -29,8 +29,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import type { WikiPage } from '@/stores/useWikiStore'
|
import type { WikiPage } from '@/stores/useWikiStore'
|
||||||
|
import { useWikiPageType } from '@/composables/useWikiPageType'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { typeColor, formatPageTypeLabel } = useWikiPageType()
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
page: WikiPage
|
page: WikiPage
|
||||||
@ -42,22 +44,6 @@ const emit = defineEmits<{
|
|||||||
(e: 'open-page', slug: string): void
|
(e: 'open-page', slug: string): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const TYPE_COLORS: Record<string, string> = {
|
|
||||||
concept: '#D96E46',
|
|
||||||
person: '#5B8DEF',
|
|
||||||
place: '#4CAF82',
|
|
||||||
event: '#F59E0B',
|
|
||||||
technology: '#8B5CF6',
|
|
||||||
organization: '#EC4899',
|
|
||||||
product: '#14B8A6',
|
|
||||||
term: '#6B7280',
|
|
||||||
process: '#F97316',
|
|
||||||
other: '#9CA3AF',
|
|
||||||
}
|
|
||||||
|
|
||||||
function typeColor(type: string | null | undefined): string {
|
|
||||||
return TYPE_COLORS[(type || 'other').toLowerCase()] || TYPE_COLORS.other
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -29,7 +29,7 @@
|
|||||||
<select :value="typeFilter" class="type-select" @change="emit('update:typeFilter', ($event.target as HTMLSelectElement).value)">
|
<select :value="typeFilter" class="type-select" @change="emit('update:typeFilter', ($event.target as HTMLSelectElement).value)">
|
||||||
<option value="">{{ t('wiki.graph.allTypes') }}</option>
|
<option value="">{{ t('wiki.graph.allTypes') }}</option>
|
||||||
<option v-for="type in availableTypes" :key="type" :value="type">
|
<option v-for="type in availableTypes" :key="type" :value="type">
|
||||||
{{ t(`wiki.pageTypes.${type}`, type) }}
|
{{ formatPageTypeLabel(type) }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<button class="btn-icon-sm" :title="t('wiki.graph.resetView')" @click="emit('reset')">
|
<button class="btn-icon-sm" :title="t('wiki.graph.resetView')" @click="emit('reset')">
|
||||||
@ -60,8 +60,10 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useWikiPageType } from '@/composables/useWikiPageType'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { formatPageTypeLabel } = useWikiPageType()
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
nodeCount: number
|
nodeCount: number
|
||||||
|
|||||||
@ -44,12 +44,14 @@ import { GraphChart } from 'echarts/charts'
|
|||||||
import { TooltipComponent, LegendComponent } from 'echarts/components'
|
import { TooltipComponent, LegendComponent } from 'echarts/components'
|
||||||
import { CanvasRenderer } from 'echarts/renderers'
|
import { CanvasRenderer } from 'echarts/renderers'
|
||||||
import type { WikiPage } from '@/stores/useWikiStore'
|
import type { WikiPage } from '@/stores/useWikiStore'
|
||||||
|
import { useWikiPageType } from '@/composables/useWikiPageType'
|
||||||
import WikiGraphToolbar from './WikiGraphToolbar.vue'
|
import WikiGraphToolbar from './WikiGraphToolbar.vue'
|
||||||
import WikiGraphNodePanel from './WikiGraphNodePanel.vue'
|
import WikiGraphNodePanel from './WikiGraphNodePanel.vue'
|
||||||
|
|
||||||
echarts.use([GraphChart, TooltipComponent, LegendComponent, CanvasRenderer])
|
echarts.use([GraphChart, TooltipComponent, LegendComponent, CanvasRenderer])
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { typeColor, formatPageTypeLabel } = useWikiPageType()
|
||||||
const props = defineProps<{ pages: WikiPage[] }>()
|
const props = defineProps<{ pages: WikiPage[] }>()
|
||||||
const emit = defineEmits<{ (e: 'open-page', slug: string): void }>()
|
const emit = defineEmits<{ (e: 'open-page', slug: string): void }>()
|
||||||
|
|
||||||
@ -62,23 +64,6 @@ const showOrphans = ref(true)
|
|||||||
const typeFilter = ref('')
|
const typeFilter = ref('')
|
||||||
const selectedNode = ref<WikiPage | null>(null)
|
const selectedNode = ref<WikiPage | null>(null)
|
||||||
|
|
||||||
// Type → color map
|
|
||||||
const TYPE_COLORS: Record<string, string> = {
|
|
||||||
concept: '#D96E46',
|
|
||||||
person: '#5B8DEF',
|
|
||||||
place: '#4CAF82',
|
|
||||||
event: '#F59E0B',
|
|
||||||
technology: '#8B5CF6',
|
|
||||||
organization: '#EC4899',
|
|
||||||
product: '#14B8A6',
|
|
||||||
term: '#6B7280',
|
|
||||||
process: '#F97316',
|
|
||||||
other: '#9CA3AF',
|
|
||||||
}
|
|
||||||
|
|
||||||
function typeColor(type: string | null | undefined): string {
|
|
||||||
return TYPE_COLORS[(type || 'other').toLowerCase()] || TYPE_COLORS.other
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse outgoing links JSON string → slug[]
|
// Parse outgoing links JSON string → slug[]
|
||||||
function parseLinks(outgoingLinks: string | null | undefined): string[] {
|
function parseLinks(outgoingLinks: string | null | undefined): string[] {
|
||||||
@ -245,7 +230,7 @@ function buildOption() {
|
|||||||
// Look up from slugToPage instead of relying on _page in ECharts data
|
// Look up from slugToPage instead of relying on _page in ECharts data
|
||||||
const page = slugToPage.value.get(params.data.id)
|
const page = slugToPage.value.get(params.data.id)
|
||||||
if (!page) return ''
|
if (!page) return ''
|
||||||
const typeLabel = t(`wiki.pageTypes.${page.pageType || 'other'}`, page.pageType || 'other')
|
const typeLabel = formatPageTypeLabel(page.pageType || 'other')
|
||||||
const summary = (page.summary || '').substring(0, 80)
|
const summary = (page.summary || '').substring(0, 80)
|
||||||
const ellipsis = (page.summary || '').length > 80 ? '…' : ''
|
const ellipsis = (page.summary || '').length > 80 ? '…' : ''
|
||||||
return [
|
return [
|
||||||
|
|||||||
@ -186,16 +186,21 @@
|
|||||||
import { ref, reactive, computed, watch } from 'vue'
|
import { ref, reactive, computed, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useWikiStore, isProtectedPage, type WikiPage } from '@/stores/useWikiStore'
|
import { useWikiStore, isProtectedPage, type WikiPage } from '@/stores/useWikiStore'
|
||||||
|
import { useWikiPageType } from '@/composables/useWikiPageType'
|
||||||
import { wikiApi } from '@/api/index'
|
import { wikiApi } from '@/api/index'
|
||||||
|
|
||||||
const emit = defineEmits<{ (e: 'open-page', slug: string): void }>()
|
const emit = defineEmits<{ (e: 'open-page', slug: string): void }>()
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const store = useWikiStore()
|
const store = useWikiStore()
|
||||||
|
const { formatPageTypeLabel } = useWikiPageType()
|
||||||
const pageListEl = ref<HTMLElement | null>(null)
|
const pageListEl = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
const PAGE_STEP = 20
|
const PAGE_STEP = 20
|
||||||
const TYPE_ORDER = ['concept', 'technology', 'process', 'person', 'organization', 'product', 'place', 'event', 'term', 'other']
|
|
||||||
|
// Group ordering follows the KB profile's declared pageType order; types not in
|
||||||
|
// the profile (synthesis/system or stale types from a prior profile) sort after.
|
||||||
|
const typeOrder = computed(() => store.pageTypeProfile?.order ?? [])
|
||||||
|
|
||||||
const pageSearch = ref('')
|
const pageSearch = ref('')
|
||||||
const searchPageLimit = ref(PAGE_STEP)
|
const searchPageLimit = ref(PAGE_STEP)
|
||||||
@ -226,11 +231,14 @@ const groupedPages = computed(() => {
|
|||||||
if (!map.has(type)) map.set(type, [])
|
if (!map.has(type)) map.set(type, [])
|
||||||
map.get(type)!.push(page)
|
map.get(type)!.push(page)
|
||||||
}
|
}
|
||||||
|
const order = typeOrder.value
|
||||||
return [...map.entries()]
|
return [...map.entries()]
|
||||||
.sort(([a], [b]) => {
|
.sort(([a], [b]) => {
|
||||||
const ia = TYPE_ORDER.indexOf(a) >= 0 ? TYPE_ORDER.indexOf(a) : 99
|
const ia = order.indexOf(a) >= 0 ? order.indexOf(a) : 99
|
||||||
const ib = TYPE_ORDER.indexOf(b) >= 0 ? TYPE_ORDER.indexOf(b) : 99
|
const ib = order.indexOf(b) >= 0 ? order.indexOf(b) : 99
|
||||||
return ia - ib
|
if (ia !== ib) return ia - ib
|
||||||
|
// Stable, deterministic tail ordering for types outside the profile.
|
||||||
|
return a.localeCompare(b)
|
||||||
})
|
})
|
||||||
.map(([type, pages]) => ({ type, pages }))
|
.map(([type, pages]) => ({ type, pages }))
|
||||||
})
|
})
|
||||||
@ -267,9 +275,7 @@ function paginatedGroupPages(group: { type: string; pages: any[] }) {
|
|||||||
|
|
||||||
function formatGroupLabel(type: string): string {
|
function formatGroupLabel(type: string): string {
|
||||||
if (!type) return t('wiki.pageTypes.other')
|
if (!type) return t('wiki.pageTypes.other')
|
||||||
const key = `wiki.pageTypes.${type.toLowerCase()}`
|
return formatPageTypeLabel(type)
|
||||||
const translated = t(key)
|
|
||||||
return translated === key ? (type.charAt(0).toUpperCase() + type.slice(1)) : translated
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSelect(slug: string) {
|
function toggleSelect(slug: string) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user