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:
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user