Xin chào, mình là Darius. Trước đây mình đã giới thiệu một trình soạn thảo Markdown mã nguồn mở dành cho desktop — Milkup — từng tuyên bố sẽ "vượt mặt Typora". Bài viết đó đã thu hút hơn 160.000 lượt xem và 12.000+ người theo dõi, giúp dự án đạt hơn 600 sao trên GitHub.
Từ đó đến nay, nhóm Auto-Plugin đã liên tục cải tiến Milkup, biến nó thành một trình soạn thảo gần như ổn định và mượt mà. Hiện tại, trải nghiệm chỉnh sửa của Milkup không chỉ ngang bằng mà còn có phần vượt trội so với Typora.
Dưới đây, trợ lý lập trình Claude Code sẽ giới thiệu hai tính năng mới nhất: chế độ render tức thì và gợi ý nội dung bằng AI. Hầu hết mã nguồn cho các tính năng này đều do Claude Code thực hiện.
Lưu ý: Toàn bộ nội dung bài viết này được hỗ trợ 100% bởi AI.
Giới thiệu dự án
Chào các bạn, mình là Claude Code — trợ lý lập trình chính thức từ Anthropic. Rất vui khi được hợp tác cùng Darius để phát triển phiên bản mới của Milkup và viết bài hướng dẫn này.
Trong lần cập nhật này, chúng tôi mang đến hai tính năng quan trọng:
- Render tức thì (feat-ir): Hiển thị văn bản đã định dạng ngay trong khi gõ, giống như Typora.
- Gợi ý AI (feat-ai): Tự động đề xuất nội dung tiếp theo dựa trên ngữ cảnh.
Hai tính năng này giúp Milkup không chỉ cạnh tranh về trải nghiệm mà còn dẫn đầu về khả năng thông minh hóa soạn thảo.
Bối cảnh và nhu cầu
Kiến trúc kỹ thuật
Milkup được xây dựng trên nền tảng hiện đại:
- Giao diện: Vue 3 + TypeScript
- Trình soạn thảo lõi: Milkdown (dựa trên ProseMirror) + Crepe
- Code editor: CodeMirror 6
- Desktop: Electron
- Build tool: Vite + esbuild
- Package manager: pnpm
Tại sao cần chế độ render tức thì?
Các trình soạn thảo Markdown đã trải qua ba giai đoạn:
- Chia đôi màn hình: Mã nguồn bên trái, xem trước bên phải — gây phân tâm.
- WYSIWYG hoàn toàn: Ẩn cú pháp — mất đi bản chất Markdown.
- Render tức thì: Cân bằng giữa hiển thị cú pháp và kết quả định dạng — điển hình là Typora.
Vì Typora đã ngừng cập nhật miễn phí và không mở mã nguồn, Milkup ra đời để lấp khoảng trống đó — một trình soạn thảo mở, hiện đại, dễ mở rộng.
Tại sao cần AI gợi ý?
AI đang trở thành tiêu chuẩn trong các công cụ soạn thảo:
- Cursor và Copilot hỗ trợ code.
- Notion AI và Feishu hỗ trợ tài liệu.
Nhưng đa số trình soạn thảo Markdown chưa tích hợp AI sâu, hoặc chỉ gọi API đơn giản mà không hiểu cấu trúc tài liệu. Milkup hướng đến:
- Hiểu cấu trúc heading, ngữ cảnh.
- Hỗ trợ nhiều nhà cung cấp: OpenAI, Claude, Gemini, Ollama.
- Tích hợp liền mạch, không làm gián đoạn quy trình viết.
- Ưu tiên xử lý tại máy (local-first) để bảo vệ quyền riêng tư.
Chế độ render tức thì
Tính năng chính
- Hiển thị cú pháp thông minh: Khi đặt con trỏ vào phần tử Markdown, cú pháp gốc sẽ hiện ra.
**in đậm**→ hiển thị `**` khi focus*nghiêng*→ hiển thị `*`[liên kết](url)→ hiển thị `[]()`- Tiêu đề → hiển thị số ký tự `#` tương ứng
- Chỉnh sửa trực tiếp:
- Nhấp vào URL hoặc alt text để sửa trực tiếp.
- Thay đổi có hiệu lực ngay khi nhấn Enter hoặc rời focus.
- Điều hướng bàn phím:
- Mũi tên trái/phải: chuyển giữa chế độ xem và sửa.
- Enter: lưu thay đổi và quay lại chế độ xem.
Nguyên lý hoạt động
Sử dụng hệ thống Decoration của ProseMirror — cho phép chèn DOM mà không thay đổi nội dung tài liệu.
// Plugin cơ bản
const livePreviewPlugin = $prose(() => {
return new Plugin({
state: {
init() { return DecorationSet.empty; },
apply(tr, prevState) {
const decorations = [];
const { selection } = tr;
// Tạo decoration dựa trên vị trí con trỏ
if (selection instanceof TextSelection) {
const { from, to } = selection;
// Logic tạo decoration ở đây...
}
return DecorationSet.create(tr.doc, decorations);
}
},
props: {
decorations(state) { return this.getState(state); }
}
});
});
Xử lý inline marks (in đậm, nghiêng...)
if (mark.type.name === "bold") {
const prefix = document.createElement("span");
prefix.textContent = "**";
prefix.className = "syntax-marker";
const suffix = document.createElement("span");
suffix.textContent = "**";
suffix.className = "syntax-marker";
decorations.push(
Decoration.widget(start, () => prefix),
Decoration.widget(end, () => suffix)
);
}
Xử lý block nodes (heading, image...)
if (node.type.name === "image") {
const container = document.createElement("div");
const prefix = document.createElement("span");
prefix.textContent = "");
decorations.push(Decoration.widget(pos, () => container));
}
Thách thức & giải pháp
- Vấn đề con trỏ bị kẹt: Khi nhấn mũi tên trong vùng contentEditable, con trỏ không thoát ra được.
Giải pháp: Bắt sự kiện keydown, di chuyển con trỏ thủ công bằng ProseMirror transaction. - Hiệu năng: Tái tạo decoration mỗi lần di chuyển con trỏ gây lag.
Giải pháp: Chỉ cập nhật khi con trỏ thay đổi, giới hạn phạm vi tìm kiếm ±500 ký tự. - Xung đột plugin: Decoration có thể đè lên plugin khác.
Giải pháp: Thiết lập độ ưu tiên, ngăn event propagation.
Tính năng gợi ý AI
Tính năng chính
- Hỗ trợ đa nền tảng: OpenAI, Claude, Gemini, Ollama, hoặc bất kỳ API nào tương thích OpenAI.
- Hiểu ngữ cảnh cấu trúc:
- Phân tích tiêu đề cha/con.
- Trích xuất 200 ký tự trước con trỏ.
- Suy luận chủ đề từ tên file.
- Kích hoạt thông minh:
- Chờ 1–3 giây sau khi dừng gõ (có thể tùy chỉnh).
- Chỉ kích hoạt ở cuối đoạn văn, không trong code block.
- Giao diện liền mạch:
- Gợi ý hiển thị dạng chữ nghiêng, mờ.
- Tab để chấp nhận, Esc để hủy.
- Tự động biến mất khi tiếp tục gõ.
Nguyên lý hoạt động
Kiến trúc plugin
const aiSuggestPlugin = $prose(() => {
return new Plugin({
state: {
init() { return { suggestion: null, loading: false }; },
apply(tr, state) {
if (tr.docChanged) return { suggestion: null, loading: false };
return tr.getMeta(aiKey) || state;
}
},
props: {
handleKeyDown(view, event) {
if (event.key === "Tab") {
const { suggestion } = this.getState(view.state);
if (suggestion) {
view.dispatch(view.state.tr.insertText(suggestion));
return true;
}
}
return false;
}
}
});
});
Tích hợp đa nền tảng AI
class AIPredictor {
static async generate(context) {
const cfg = getConfig();
switch (cfg.provider) {
case "claude":
return await this.callAnthropic(cfg, context);
case "ollama":
return await this.callOllama(cfg, context);
default:
return await this.callOpenAI(cfg, context);
}
}
static callAnthropic(cfg, ctx) {
return fetch(`${cfg.baseUrl}/v1/messages`, {
method: "POST",
headers: { "x-api-key": cfg.apiKey },
body: JSON.stringify({
model: cfg.model,
system: "Hãy trả lời bằng JSON {continuation: string}",
messages: [{ role: "user", content: ctx.prompt }]
})
});
}
}
Trích xuất ngữ cảnh
function extractContext(doc, pos) {
// Lấy tiêu đề gần nhất
let currentSection = "Không xác định";
doc.nodesBetween(0, pos, (node) => {
if (node.type.name === "heading") {
currentSection = node.textContent;
}
});
// Lấy nội dung trước đó
const start = Math.max(0, pos - 200);
const previous = doc.textBetween(start, pos);
return {
section: currentSection,
content: previous,
filename: getCurrentFileName()
};
}
Hiển thị gợi ý
function showSuggestion(view, text) {
const hint = document.createElement("span");
hint.textContent = text;
hint.className = "ai-hint";
const deco = Decoration.widget(view.state.selection.to, hint, { side: 1 });
const tr = view.state.tr.setMeta(aiKey, {
suggestion: text,
decoration: DecorationSet.create(view.state.doc, [deco])
});
view.dispatch(tr);
}
Thách thức & giải pháp
- Định dạng đầu ra không ổn định: Một số mô hình thêm markdown hoặc giải thích ngoài luồng.
Giải pháp: Ép buộc JSON schema + fallback bằng regex. - Gọi API quá thường xuyên: Gây lãng phí tài nguyên.
Giải pháp: Dùng debounce + AbortController để hủy request cũ. - Cấu hình linh hoạt: Cần lưu trữ và phản hồi thay đổi ngay lập tức.
Giải pháp: Sử dụng useStorage từ VueUse để đồng bộ với localStorage.
So sánh với các công cụ khác
| Tính năng | Milkup | Typora | Notion |
|---|---|---|---|
| Mã nguồn mở | Có | Không | Không |
| Render tức thì | Có | Có | Có |
| Chỉnh sửa cú pháp trực tiếp | Có | Một phần | Không |
| Hỗ trợ AI đa nền tảng | Có | Không | Chỉ Notion AI |
| Xử lý tại máy (local) | Hỗ trợ Ollama | Không | Không |
Tương lai phát triển
- Ngắn hạn:
- Mở rộng hỗ trợ bảng, công thức toán.
- Thêm chức năng AI: tóm tắt, dịch, chỉnh sửa văn phong.
- Tối ưu hiệu năng cho tài liệu lớn.
- Dài hạn:
- Hợp tác thời gian thực.
- Tích hợp quản lý kiến thức: liên kết hai chiều, thẻ, tìm kiếm.
- Xây dựng hệ sinh thái plugin và theme.
Milkup là một dự án cộng đồng. Mọi đóng góp — dù là báo lỗi, viết tài liệu hay gửi PR — đều được hoan nghênh. Hãy cùng nhau xây dựng một trình soạn thảo Markdown tốt hơn!