feat: Enhance article generation and outline handling

- Added support for external markdown synchronization in ArticleEditorCanvas.vue.
- Implemented a new function to sync markdown from props, ensuring the editor reflects external changes.
- Introduced a removeOutlineSection function in TemplateWizardView.vue to manage outline sections dynamically.
- Updated UI components to improve user experience, including replacing a-input with a-textarea for better text handling.
- Refactored CSS styles for a cleaner layout and improved responsiveness in TemplateWizardView.vue.
- Enhanced LLM generation requests to include response format options in ark.go and client.go.
- Updated template assist logic to enforce structured outline outputs in template_assist.go.
- Added tests for outline result decoding and prompt generation to ensure robustness.
- Created migration scripts to update prompt templates with new writing requirements for top X articles.
This commit is contained in:
2026-04-02 10:58:39 +08:00
parent b31d8d0096
commit 7fa1809682
12 changed files with 605 additions and 106 deletions
@@ -20,7 +20,7 @@ import {
wrapInHeadingCommand,
wrapInOrderedListCommand,
} from "@milkdown/kit/preset/commonmark";
import { callCommand } from "@milkdown/kit/utils";
import { callCommand, replaceAll } from "@milkdown/kit/utils";
import { Crepe, CrepeFeature } from "@milkdown/crepe";
import "@milkdown/crepe/theme/common/style.css";
import "@milkdown/crepe/theme/frame.css";
@@ -41,6 +41,7 @@ const emit = defineEmits<{
const { t } = useI18n();
const crepeRef = ref<Crepe | null>(null);
const syncingExternalMarkdown = ref(false);
const { loading } = useEditor((root) => {
const crepe = new Crepe({
@@ -51,12 +52,17 @@ const { loading } = useEditor((root) => {
[CrepeFeature.Toolbar]: true,
[CrepeFeature.Latex]: false,
[CrepeFeature.BlockEdit]: false,
[CrepeFeature.Placeholder]: false,
},
});
crepe.setReadonly(!!props.disabled);
crepe.on((listener) => {
listener.markdownUpdated((_ctx, markdown) => {
if (syncingExternalMarkdown.value) {
return;
}
emit("update:modelValue", markdown);
});
});
@@ -68,6 +74,25 @@ const { loading } = useEditor((root) => {
const [instanceLoading, getEditor] = useInstance();
const editorDisabled = computed(() => props.disabled || instanceLoading.value || loading.value);
function syncMarkdownFromProps(nextValue: string): void {
const editor = getEditor();
const crepe = crepeRef.value;
if (!editor || !crepe) {
return;
}
const resolvedValue = nextValue || "";
if (crepe.getMarkdown() === resolvedValue) {
return;
}
syncingExternalMarkdown.value = true;
editor.action(replaceAll(resolvedValue, true));
queueMicrotask(() => {
syncingExternalMarkdown.value = false;
});
}
watch(
() => props.disabled,
(value) => {
@@ -76,6 +101,18 @@ watch(
{ immediate: true },
);
watch(
[() => props.modelValue, loading, instanceLoading],
([value, editorLoading, currentInstanceLoading]) => {
if (editorLoading || currentInstanceLoading) {
return;
}
syncMarkdownFromProps(value);
},
{ immediate: true },
);
onBeforeUnmount(() => {
crepeRef.value = null;
});
+90 -48
View File
@@ -1276,6 +1276,18 @@ function addCustomOutlineSection(): void {
customOutlineInput.value = "";
}
function removeOutlineSection(key: string): void {
outlineSections.value = outlineSections.value.filter((item) => item !== key);
const target = outlineOptions.value.find((item) => item.key === key);
if (!target?.custom) {
return;
}
customOutlineSections.value = customOutlineSections.value.filter((item) => item.key !== key);
outlineOptionOrder.value = outlineOptionOrder.value.filter((item) => item !== key);
}
function sortOutlineOptions(
options: ResolvedOutlineOption[],
order: string[],
@@ -2035,12 +2047,6 @@ function moveArrayItemBefore<T>(items: T[], sourceIndex: number, targetIndex: nu
return next;
}
function outlineInputWidth(value: string): string {
const length = Array.from(value.trim()).length;
const widthEm = Math.min(Math.max(length + 4, 14), 42);
return `${widthEm}em`;
}
function onStructureDragStart(event: DragEvent, key: string): void {
if (!event.dataTransfer) {
return;
@@ -2388,9 +2394,20 @@ function onStructureDragEnd(): void {
>
{{ section.label }}
</a-checkbox>
<span class="outline-option__handle">
<HolderOutlined />
</span>
<div class="outline-option__actions">
<a-button
type="text"
danger
size="small"
:title="t('common.delete')"
@click.stop="removeOutlineSection(section.key)"
>
<template #icon><DeleteOutlined /></template>
</a-button>
<span class="outline-option__handle">
<HolderOutlined />
</span>
</div>
</div>
</div>
@@ -2476,10 +2493,10 @@ function onStructureDragEnd(): void {
@drop="onOutlineDrop($event, section.id, 'branch', null)"
>
<span class="outline-branch__dot" />
<a-input
<a-textarea
v-model:value="section.outline"
class="outline-input"
:style="{ '--outline-input-width': outlineInputWidth(section.outline) }"
:auto-size="{ minRows: 1 }"
:placeholder="t('templates.wizard.placeholders.outlineNode')"
/>
<div class="outline-node-actions">
@@ -2511,10 +2528,10 @@ function onStructureDragEnd(): void {
@drop="onOutlineDrop($event, child.id, 'leaf', section.id)"
>
<span class="outline-leaf__dot" />
<a-input
<a-textarea
v-model:value="child.outline"
class="outline-input"
:style="{ '--outline-input-width': outlineInputWidth(child.outline) }"
:auto-size="{ minRows: 1 }"
:placeholder="t('templates.wizard.placeholders.outlineNode')"
/>
<div class="outline-node-actions">
@@ -2657,9 +2674,7 @@ function onStructureDragEnd(): void {
display: flex;
flex-direction: column;
min-height: calc(100vh - 64px);
background:
radial-gradient(circle at top left, rgba(75, 132, 255, 0.08), transparent 28%),
linear-gradient(180deg, #f5f8ff 0%, #f7f9fc 100%);
background: #fff;
}
.wizard-page__loading {
@@ -2671,10 +2686,9 @@ function onStructureDragEnd(): void {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
border-bottom: 1px solid rgba(22, 119, 255, 0.08);
backdrop-filter: blur(14px);
background: rgba(255, 255, 255, 0.82);
padding: 16px 32px;
border-bottom: 1px solid #edf2f7;
background: #fff;
}
.header-left {
@@ -2691,9 +2705,9 @@ function onStructureDragEnd(): void {
.header-title {
margin: 0;
color: #10203d;
color: #111827;
font-size: 20px;
font-weight: 700;
font-weight: 800;
}
.header-accent {
@@ -2712,27 +2726,26 @@ function onStructureDragEnd(): void {
.wizard-page__main {
display: flex;
flex-direction: column;
gap: 20px;
margin: 24px;
margin: 0 auto;
width: 100%;
max-width: 1160px;
padding: 24px 32px 64px;
}
.wizard-steps-container {
padding: 20px 24px;
border: 1px solid rgba(20, 44, 88, 0.08);
border-radius: 24px;
background: rgba(255, 255, 255, 0.86);
box-shadow: 0 18px 48px rgba(17, 39, 81, 0.06);
padding: 0 0 24px;
border-bottom: 1px solid #edf2f7;
}
.wizard-section {
display: flex;
flex-direction: column;
gap: 20px;
}
.wizard-section--structure {
display: grid;
grid-template-columns: minmax(0, 1fr) 600px;
grid-template-columns: minmax(0, 1fr) 460px;
gap: 48px;
align-items: start;
}
@@ -2744,11 +2757,13 @@ function onStructureDragEnd(): void {
}
.wizard-card {
padding: 24px;
border: 1px solid rgba(20, 44, 88, 0.08);
border-radius: 28px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(247, 250, 255, 0.96) 100%);
box-shadow: 0 18px 48px rgba(17, 39, 81, 0.06);
padding: 32px 0;
border-bottom: 1px solid #edf2f7;
}
.wizard-section > .wizard-card:last-child,
.wizard-structure-main > .wizard-card:last-child {
border-bottom: none;
}
.wizard-card__header {
@@ -2756,20 +2771,33 @@ function onStructureDragEnd(): void {
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
margin-bottom: 24px;
}
.wizard-card__header h3 {
margin: 0;
color: #12213f;
font-size: 18px;
font-weight: 700;
color: #111827;
font-size: 16px;
font-weight: 800;
display: flex;
align-items: center;
}
.wizard-card__header h3::before {
content: "";
display: inline-block;
width: 4px;
height: 16px;
border-radius: 2px;
background: #111827;
margin-right: 12px;
}
.wizard-card__header p {
margin: 6px 0 0;
color: #71809a;
margin: 6px 0 0 16px;
color: #667085;
font-size: 13px;
line-height: 1.7;
}
.field-grid {
@@ -2789,9 +2817,16 @@ function onStructureDragEnd(): void {
}
.field-item > label {
color: #22304d;
font-size: 14px;
font-weight: 600;
display: inline-flex;
align-items: center;
margin-bottom: 12px;
color: #1f2937;
font-size: 15px;
font-weight: 700;
}
.field-item > label::after {
content: "";
}
.required-asterisk::before {
@@ -3059,6 +3094,12 @@ function onStructureDragEnd(): void {
cursor: grab;
}
.outline-option__actions {
display: flex;
align-items: center;
margin-left: 12px;
}
.custom-outline-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
@@ -3153,14 +3194,14 @@ function onStructureDragEnd(): void {
}
.outline-input {
width: min(100%, var(--outline-input-width, 18em));
flex: 1;
min-width: 0;
flex-shrink: 1;
border-color: transparent !important;
background-color: transparent !important;
box-shadow: none !important;
transition: all 0.2s;
padding: 4px 8px;
resize: none;
}
.outline-input:hover {
@@ -3185,7 +3226,8 @@ function onStructureDragEnd(): void {
.preview-card {
position: sticky;
top: 96px;
top: 32px;
border-bottom: none;
}
.preview-shell {