<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Tai Phan</title>
    <link>https://taiphanvan.dev</link>
    <description>Canvas cá nhân của Tai — engineer ở Việt Nam, làm sản phẩm. Tech, AI, phim, sách, ảnh, game, bất cứ gì muốn ghi.</description>
    <language>vi-VN</language>
    <lastBuildDate>Thu, 29 Oct 2026 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://taiphanvan.dev/rss.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title>Bạch Đằng: tập làm game bằng rối nước</title>
      <link>https://taiphanvan.dev/vi/blog/bach-dang-tap-lam-game-bang-roi-nuoc</link>
      <guid isPermaLink="true">https://taiphanvan.dev/vi/blog/bach-dang-tap-lam-game-bang-roi-nuoc</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <description>Game đầu tay của mình: trận Bạch Đằng năm 938 kể lại bằng rối nước, tranh khắc gỗ và những con nước phải canh đúng nhịp.</description>
      <content:encoded><![CDATA[<p>Mình chưa làm game bao giờ. App thì ship được mấy cái rồi, nhưng game là một thế giới khác hẳn: có kịch bản, có nhịp, có cái "game feel" mà trước giờ mình chỉ biết qua... chơi =)). Một tháng nay mình tập tành làm game đầu tay, tên là <a href="/work/bach-dang">Bạch Đằng</a>.</p>
<p>Chủ đề thì không phải nghĩ lâu. Sử Việt có những trận đánh ai cũng thuộc — bãi cọc, con nước, thuyền giặc mắc cạn — nhưng hiếm khi được kể bằng game. Còn hình ảnh thì có sẵn một thứ rất Việt Nam mà người nước ngoài nhìn là nhận ra: múa rối nước.</p>
<h2>Game kể chuyện trên sông Bạch Đằng</h2>
<p>Bạch Đằng là game kể chuyện có lựa chọn. Mỗi chương là một trận trên sông Bạch Đằng: năm 938 Ngô Quyền, năm 981 Lê Hoàn, năm 1288 Trần Hưng Đạo. Hiện mình mới làm chương 1, tên ngắn gọn: <strong>938</strong>.</p>
<p>Các bạn vào vai Dương Lãng, con trai một người chài bị cả làng coi là "kẻ dẫn giặc". Lãng được Dương Đình Nghệ nhận làm con nuôi, rồi phải đọc con nước để giăng bẫy cọc cho Ngô Quyền — trong khi người cha tưởng đã chết lại đang đứng mũi hạm đội Nam Hán. Câu hỏi xuyên suốt chương: <em>"Làm con thì nợ ai?"</em></p>
<p>Một lần chơi khoảng 35–45 phút, gồm 22 cảnh, 5 quyết định lớn và 7 kết cục.</p>
<h2>Rối nước, tranh khắc gỗ và chú Tễu</h2>
<p>Phần mình khoái nhất là cách diễn. Nhân vật là con rối có khớp — đầu, thân, tay hai khúc — nhấp nhô như trên mặt nước, diễn trên sân khấu thủy đình. Màu lấy từ tranh khắc gỗ dân gian: giấy điệp, đỏ son, vàng hoè, chàm. Nhạc là đàn bầu, sáo trúc, trống, chiêng, điệu hò kéo lưới.</p>
<p>Như một buổi rối nước thật, <strong>chú Tễu</strong> ra mở màn và khép màn. Tễu vẽ theo lối tranh Đông Hồ, đứng ngoài truyện. Giữa các hồi, khi rèm đỏ son kéo kín, Tễu ra pha vài câu hóm hỉnh với bà con — nhưng không bao giờ đùa về cái chết hay về sử.</p>
<p>Game cũng theo nguyên tắc "diễn chứ không kể": cảnh tự chạy theo đoạn như xem phim, phụ đề tối đa 2 hàng, người chơi chỉ chạm ở cuối đoạn, lúc chọn và lúc tương tác. Cảnh 1 dài khoảng 3 phút mà chỉ cần 4 lần chạm.</p>
<h2>Lịch sử là xương sống</h2>
<p>Làm game về sử rất dễ trượt sang một trong hai cực: bóp méo cho kịch tính, hoặc giọng tuyên truyền. Nên trước khi viết cảnh nào, mình chốt mấy luật:</p>
<ul>
<li><strong>Chính sử giữ nguyên ở mọi nhánh</strong>: người chơi chỉ quyết định trong những khoảng trống mà sử không ghi.</li>
<li><strong>Hư cấu có nhãn</strong>: mỗi chi tiết gắn tầng nguồn — [CS] chính sử, [TT] thần tích, [SL] suy luận hiện đại, [HC] hư cấu.</li>
<li><strong>Đối thủ là người thật</strong>: phía Nam Hán có người khôn kẻ dại, có lời can đúng. Thắng một đối thủ đáng gờm thì chiến thắng mới có sức nặng.</li>
<li><strong>Thua cũng không đổi sử</strong>: kết cục xấu luôn khép bằng câu <em>"Nhưng lịch sử đã không diễn ra như thế."</em></li>
</ul>
<p>Cuối chương có màn <strong>"Lịch sử thật"</strong> tách bạch đâu là sử chép, đâu là game viết thêm. Có một chi tiết khá vui: Hoằng Tháo chết ở Bạch Đằng, nhưng Toàn thư, Cương mục và Tân Ngũ Đại sử mỗi bộ ghi một kiểu. Lựa chọn cuối của người chơi sẽ khớp với một trong ba cách đó.</p>
<h2>Canh con nước</h2>
<p>Ngoài lựa chọn, cơ chế chính là <strong>canh con nước</strong>, 3 lượt: kéo vạch trên cây sào để đặt độ cao cọc, xuất thuyền mồi, phất cờ xuất kích. Mực nước chạy theo nhật triều — gần đỉnh thì chậm, rút giữa chừng thì nhanh. Lựa chọn trước đó cũng ăn vào đây: càng hiểu sông, cửa sổ thời gian càng rộng.</p>
<p>Mình thích nhất chỗ lòng tin biến thành cơ chế. Khi Ngô Quyền chưa tin Lãng, người cầm cờ là Tam Kha, và lá cờ phất chậm hơn cú bấm của các bạn một nhịp.</p>
<p>Cân bằng thì cho chạy hết 81 tổ hợp quyết định: canh nước giỏi thì phần lớn đường đi tới kết cục tốt nhất, canh trung bình thì không, và không có tổ hợp nào luôn đúng.</p>
<h2>Web trước, Godot sau</h2>
<p>Bản đầu mình làm trên web — JavaScript, SVG, nhạc tổng hợp bằng Web Audio. Từ kịch bản tới đủ 22 cảnh có hoạt hình mất khoảng hai ngày, rồi mời người chơi thử ba lượt và sửa theo feedback.</p>
<p>Câu chuyện đứng được, nhưng điện thoại thì không: bố cục vỡ, nên bản web phải tạm chặn điện thoại. Thay vì đắp tiếp, mình coi bản web là một <strong>bản đặc tả chạy được</strong> và làm lại từ đầu cho iOS, Android trên Godot 4.7.</p>
<p>Lý do chọn Godot khá thực dụng. Hình trong game vẽ bằng code, nên engine nào cho làm mọi thứ bằng file chữ — cảnh, hoạt cảnh, dữ liệu — sẽ nhanh hơn hẳn engine nặng thao tác kéo thả trong editor.</p>
<p>Port 22 cảnh sang Godot chia 5 đợt, xong trong một ngày. Phần tốn công là chạy trên máy thật: con Pixel 3 cũ bị văng game ngang cảnh 9 vì rò bộ nhớ ảnh. Phải sửa rò, ẩn phần cảnh nằm dưới màn canh nước, làm shader khắc gỗ rẻ hơn và tự hạ độ nét khi tụt khung hình thì mới giữ được 60 fps.</p>
<h2>Claude làm, mình chốt</h2>
<p>Code, hình vector, nhạc tổng hợp, tài liệu — gần như toàn bộ là Claude làm. Vai của mình là định hướng và chốt: tinh thần dự án, luật với lịch sử, giọng kể, chuyện gì giữ chuyện gì cắt.</p>
<p>Mọi thứ đi qua vòng <em>phân tích → sửa → chốt</em>, và bản chốt mới nhất là chuẩn. Kịch bản viết lại ba lần trong một ngày, phân cảnh review từng cảnh một, lời 22 cảnh viết lại theo giọng người phường rối kể tích. Chú Tễu ra giữa hồi ba lần cũng là một quyết định mình chốt sau khi cân nhắc.</p>
<p><p>Claude không thay được chuyên gia sử và hải văn. Bản thử thì đủ, nhưng trước khi phát hành công
khai, game cần người có chuyên môn duyệt lại sử và con nước.</p></p>
<p>Gần đây mình cài thêm <a href="https://github.com/donchitos/claude-code-game-studios">Claude Code Game Studios</a> — bộ agent, skill, hook dựng sẵn quy trình của một studio game — để làm một vòng chơi mới: <strong>hỏi người</strong>. Ở cảnh truy kẻ nội ứng, Lãng phải hỏi từng người, nối các thẻ lời khai trong Sổ tay và bác từng lời tố bằng chứng cứ, chỉ trong 6 lượt hỏi.</p>
<h2>Sắp lên sân khấu</h2>
<p>Bước tiếp theo là mời chơi thử chương 1 trên điện thoại. Bạn nào muốn xem buổi diễn sớm thì đăng ký ở <a href="https://bachdang.taiphanvan.dev">bachdang.taiphanvan.dev</a> =)).</p>
<p>Rèm đỏ son sắp mở. Hẹn các bạn ở bài sau, khi có feedback đợt chơi thử đầu tiên.</p>]]></content:encoded>
      <category>bach-dang</category>
      <category>gamedev</category>
      <category>godot</category>
      <category>ai-coding</category>
    </item>
    <item>
      <title>Shipdeck: một mình, nhiều app</title>
      <link>https://taiphanvan.dev/vi/blog/shipdeck-mot-minh-nhieu-app</link>
      <guid isPermaLink="true">https://taiphanvan.dev/vi/blog/shipdeck-mot-minh-nhieu-app</guid>
      <pubDate>Wed, 21 Oct 2026 00:00:00 GMT</pubDate>
      <description>Nhảy qua lại giữa các product, mình cứ quên app nào đang tới đâu, còn gì phải xử lí. Nên mình build Shipdeck — một cửa sổ cho cả portfolio.</description>
      <content:encoded><![CDATA[<p>Mình đang chạy song song mấy product — QuickSpend, Examino, PES Arena, cộng thêm chính site này. Làm nhiều thứ thì vui, nhưng cái giá là chuyển qua chuyển lại: tuần này dí QuickSpend, tuần sau nhảy sang Examino, tới lúc quay lại thì chịu, không nhớ nổi app đang tới đâu, còn commit nào chưa ship, review nào chưa trả lời =)).</p>
<p>Mỗi lần quay lại là một lần khảo cổ: <code>git log</code>, mở App Store Connect, lục note cũ. Vài lần như vậy thì mình thấy không ổn, phải đổi workflow hoặc dùng tool, automation gì đó thôi. Vừa hay dạo này đang có trend build lại app từ đầu — <a href="https://getartcraft.com">ArtCraft</a> build lại cả bộ app của Adobe — nên mình cũng tự build một app mới cho nhu cầu của chính mình: Shipdeck.</p>
<h2>Shipdeck là gì</h2>
<p>Shipdeck là một app macOS native (SwiftUI) gom mọi product của mình vào một cửa sổ. Mỗi product gồm một hoặc nhiều component — iOS app, backend, landing page — nằm rải rác ở một hay nhiều repo.</p>
<p>Mở app lên, màn Overview xếp product vào 4 nhóm, kèm lý do viết rõ ràng:</p>
<ul>
<li><strong>Needs a release</strong>: kiểu "5 commits since 2.3.0 went live".</li>
<li><strong>In progress</strong>: còn file chưa commit hoặc chưa push.</li>
<li><strong>Quiet for a while</strong>: lâu rồi không có commit nào.</li>
<li><strong>All good</strong>: đã ship, không còn gì treo.</li>
</ul>
<p>Phần này chạy hoàn toàn không dùng AI. Shipdeck tự đọc git, version trong manifest, appcast của Sparkle và iTunes Lookup. Boring tech — nhanh, chắc, không tốn token.</p>
<h2>Claude đọc repo, mình quyết</h2>
<p>Thêm product thì chỉ cần chọn folder. Một session Claude Code headless, read-only sẽ survey repo: tách component, tìm command build/test, service, docs, tra app trên store. Sau đó mình review từng bước, field nào AI điền thì nguồn của nó nằm ngay cạnh, một click là thấy.</p>
<p>Survey chạy ở chế độ <code>--restricted</code>, không có shell, file bị git-ignore và file secret như <code>.env</code> đều bị chặn — kể cả tên file non-ASCII hay nằm trong submodule.</p>
<p>Code đổi thì có <strong>Sync with Code</strong>: survey lại dựa trên các commit kể từ lần trước, list từng field thay đổi (cũ → mới), mình tick cái nào thì lưu cái đó. Toàn bộ data product nằm trong một file <code>products.json</code> trên máy, không ghi gì vào repo.</p>
<h2>App Store Connect ngay cạnh code</h2>
<p>Thêm App Store Connect key (lưu trong keychain) là mỗi iOS app có tab App Store: version, review state, phased release, TestFlight build, feedback của tester, review của user. Release một version hay reply review đều làm ngay trong app, có bước confirm.</p>
<p>Không còn cảnh mở cả chục tab browser để biết app nào đang "Waiting for Review".</p>
<h2>Agent phải hỏi trước khi chạm vào Apple</h2>
<p>Đây là phần mình khoái nhất. Mỗi product mở được session Claude Code ngay trong Shipdeck (terminal embedded bằng SwiftTerm), và session vẫn chạy khi mình chuyển sang product khác.</p>
<p>Mình muốn agent dùng được Shipdeck — đọc trạng thái product, xem App Store, trả lời review — nhưng không muốn nó tự ý đụng vào Apple. Nên mọi thay đổi, dù của mình hay của agent, đều đi qua một action layer. Ý tưởng này mình mượn từ ArtCraft: mọi action là một command có id cố định, nên GUI, CLI hay MCP đều gọi chung một đường. Mỗi action có một tier:</p>
<ul>
<li><strong>read</strong>: đọc thông tin, chạy thoải mái.</li>
<li><strong>local</strong>: sửa data trên máy.</li>
<li><strong>remote</strong>: gọi ra ngoài, ví dụ reply review.</li>
<li><strong>publish</strong>: thứ user thấy được, ví dụ release version.</li>
<li><strong>owner</strong>: chỉ mình làm được.</li>
</ul>
<p>Agent gọi Shipdeck qua Unix socket, command <code>shipdeck</code> hoặc MCP server, và bị nhốt trong đúng product của session đó. Action <code>remote</code> hay <code>publish</code> sẽ nằm chờ trong approval inbox: Allow Once, Deny kèm lý do (agent đọc được lý do), hoặc Always Allow cho riêng product đó — riêng <code>publish</code> thì lần nào cũng hỏi. Mọi lần chạy đều được ghi vào <code>journal.jsonl</code> để trace lại.</p>
<p><p>Bài này đang được viết trong một session Claude Code chạy bên trong Shipdeck =)).</p></p>
<h2>3 ngày, từ 0.1.0 lên 0.6.3</h2>
<p>Code do Claude Code viết — chuyện đó giờ chẳng còn gì lạ nữa. Việc của mình là product owner: nói rõ muốn gì, dùng thật hằng ngày, thấy chỗ nào lạc quẻ thì bắt sửa. Vài chỗ mình trực tiếp yêu cầu hoặc bắt lỗi:</p>
<ul>
<li><strong>Agent không được tự ý đụng Apple</strong> → approval inbox và permission rules (0.6.0).</li>
<li><strong>AI không được tự ghi đè data</strong> → Sync with Code chỉ lưu field mình tick (0.6.1).</li>
<li><strong>Người lạ cài vào phải chạy được</strong> → kiểm tra máy chưa có Claude Code hay Command Line Tools, survey fail thì hiện rõ Claude Code in ra gì, thêm nút Send Feedback (0.6.2).</li>
<li><strong>Ô input của Claude bị trôi khi scroll</strong> → bật full-screen renderer của Claude Code để input luôn nằm dưới đáy (0.6.3).</li>
<li><strong>UI lỗi vặt</strong>: sheet Add Key bị collapse mất field, tab trên toolbar bị cắt khi chuyển product (0.4.4–0.4.5).</li>
<li><strong>Auto-update không lên bản 0.4.0</strong> → root cause là zsh không tách <code>${sha:+-f sha=$sha}</code> thành hai argument, GitHub từ chối update appcast. Một bug nhỏ xíu mà mất cả buổi.</li>
</ul>
<p>Ba ngày, 21 commit, 14 bản release có auto-update qua Sparkle, signed và notarized đàng hoàng. Tốc độ này mấy năm trước mình không dám nghĩ tới.</p>
<h2>Tiếp theo</h2>
<p>Shipdeck chưa mở public, mình vẫn đang làm tiếp vài tính năng:</p>
<ul>
<li><strong>Google Play</strong> trong Overview — đa số indie mobile dev ship cả Android, thiếu cái này thì chưa gọi là "mọi product trong một chỗ".</li>
<li><strong>Marketing</strong>: draft post từ data thật của product (version, commit từ bản trước, release notes), mình sửa rồi tự đăng.</li>
</ul>
<p>Sau đó mới mời 10–20 người dùng thật, sửa những gì họ vấp phải, rồi mới tính chuyện launch. Thước đo mình đặt ra không phải lượt tải, mà là một tuần sau họ còn mở app không.</p>
<p>Bao giờ có bản cho các bạn thử, mình sẽ viết tiếp. Còn giờ thì quay lại ship đã.</p>]]></content:encoded>
      <category>shipdeck</category>
      <category>macos</category>
      <category>ai-coding</category>
      <category>indie</category>
    </item>
    <item>
      <title>QuickSpend now really quick, finally</title>
      <link>https://taiphanvan.dev/vi/blog/quickspend-now-really-quick-finally</link>
      <guid isPermaLink="true">https://taiphanvan.dev/vi/blog/quickspend-now-really-quick-finally</guid>
      <pubDate>Sat, 23 May 2026 00:00:00 GMT</pubDate>
      <description>QuickSpend 3.0 thêm voice-first flow qua Siri + Shortcuts — chi tiêu nhanh đến mức không cần mở app. Bonus: cuộc chiến nắm tay Siri.</description>
      <content:encoded><![CDATA[<p><a href="https://apps.apple.com/us/app/quickspend-instant-tracker/id6754815176">QuickSpend</a> là app iOS mình build để log chi tiêu nhanh bằng câu nói tự nhiên — và phiên bản 3.0 sắp ship, cuối cùng cũng đúng cái tên <code>Quick</code>. Đặt tên <code>Quick</code> từ v1, sau hai phiên bản mới thấy hơi sai sự thật, giờ thì thấy hợp lí rồi =)).</p>
<p><img src="/blog/quickspend-3/frodo-finally.jpg" alt="it&#x27;s over, finally">
<em>"It's done. It's over." — Frodo, The Lord of the Rings.</em></p>
<h2>Vấn đề: app "quick" nhưng vẫn chưa đủ quick</h2>
<p>QuickSpend ban đầu mình build cho chính mình dùng — eat own dogfood, ít nhất để sản phẩm mọi người dùng được thì mình phải dùng được và thấy ổn trước đã. Dùng vài tháng thì lộ ra một điều khá đau lòng: chính mình vẫn quên ghi chi tiêu đều đặn =)).</p>
<p>Lý do thì quá đơn giản — vấn đề chắc chắn ai cũng gặp: quá lười, quá vội để note lại chi tiêu.</p>
<p>Cuối tháng nhìn lại chart, data thưa như tóc mình sau 30 tuổi. Vậy là vấn đề không nằm ở app, nó nằm ở <strong>bước phải mở app</strong>. Cái cần loại bỏ chính là cái đó. Mình muốn một flow mà user chỉ cần nói "chi tiêu nhanh 50k trà sữa" là xong, không cần tap gì hết, không cần nhìn màn hình, đẹp như mơ.</p>
<h2>Hướng xử lý: đẩy entry point ra khỏi app</h2>
<p>Trên iOS có hai con đường rõ ràng:</p>
<ul>
<li><strong>Nói thẳng với Siri</strong> — kịch bản đẹp nhất, không tap gì hết, sang chảnh như quảng cáo Apple.</li>
<li><strong>Shortcuts</strong> — backup thực dụng cho khi Siri trở chứng. Trigger được bằng tap, widget, hoặc gọi qua Siri đều ngon.</li>
</ul>
<p>Siri thì... vẫn ngu ngu, đôi khi mình hỏi giờ Hà Nội còn trả lời sai múi giờ =)). Nhưng Shortcuts là vũ khí mạnh thật trong hệ sinh thái Apple — một khi setup xong nó chỉ là một nút bấm. Nên mình build cả hai, dùng Shortcuts làm safety net để khi Siri drop ball thì user vẫn không bị chặn.</p>
<h2>Luồng mới: hai kịch bản</h2>
<h3>Kịch bản 1 — qua Shortcut (path luôn work)</h3>
<blockquote>
<p>"Hey Siri, chi tiêu nhanh."</p>
</blockquote>
<p>Siri trigger shortcut → shortcut hỏi lại "nội dung gì?" → user nói câu tự nhiên (ví dụ "50k trà sữa") → loading → AI parser xử lý.</p>
<p>Hai nhánh tuỳ confidence của parser:</p>
<ul>
<li><strong>Confidence cao</strong>: app lưu luôn, Siri thông báo "đã ghi", end flow. Không cần mở app, không cần nhìn màn hình, đẹp như mơ.</li>
<li><strong>Confidence thấp</strong>: app mở ra với UI listing transaction được parse, user confirm lưu hoặc cancel. App không tự tin thì cứ hỏi, hơn là lưu bừa rồi user phải vào sửa.</li>
</ul>
<h3>Kịch bản 2 — gọi thẳng app qua Siri (path đôi khi work)</h3>
<blockquote>
<p>"Hey Siri, dùng Chi tiêu nhanh để thêm chi tiêu 50k uống trà sữa."</p>
</blockquote>
<p>Kịch bản đẹp: Siri hiểu intent, gọi thẳng app, app nhận text Siri đã transcribe sẵn → AI parser xử lý y hệt luồng trên. <strong>Đỡ hẳn một bước</strong> — user không phải đợi Siri hỏi lại "nội dung gì". Sướng phải không nào?</p>
<p>Đời không như là mơ. Mình test khá nhiều lần, kết quả không hề ổn định =)). Mà oan cho Siri một phần — đào sâu vào API mới phát hiện đây không hoàn toàn lỗi của em ấy: <code>AppShortcutsProvider</code> của Apple chỉ cho phép phrase có parameter khi parameter là <code>AppEntity</code> hoặc <code>AppEnum</code>. Với free-form <code>String</code> (như nội dung chi tiêu), phrase <strong>bắt buộc</strong> phải open-ended — Siri trigger intent rồi mới hỏi lại "What expense should I add?". Mình sẽ nói thêm ở phần kỹ thuật bên dưới.</p>
<p>Hệ quả: câu kiểu "Hey Siri, dùng Chi tiêu nhanh thêm 50k trà sữa" về mặt API là không declarable được, Siri đoán mò. Đó là lý do Shortcut variant tồn tại — nó là path mình tin được, khi Siri đang có hứng làm việc thì kịch bản 2 mới đẹp.</p>
<p>Nghe đồn với Apple Intelligence thì Siri sẽ thông minh hơn, hy vọng là vậy để kịch bản 2 ổn định hơn trong tương lai. Hiện tại thì mình chưa test được vì đang chỉ có iPhone 15, bạn nào có iPhone 16 Pro hoặc 17 thử giúp mình xem Apple Intelligence có cứu được kịch bản 2 không nhé =)).</p>
<h2>Mấy vấn đề khi build feature này</h2>
<p><strong>Đồng bộ ngôn ngữ giữa Siri và app</strong>. Trước đó QuickSpend tách ngôn ngữ hiển thị và ngôn ngữ transcribe — user có thể chạy UI tiếng Anh nhưng voice input tiếng Việt, kiểu menu Tây mà order Việt. Khi thêm Siri vào, separation đó vỡ — Siri chỉ nói một ngôn ngữ tại một thời điểm theo system. Mình phải bỏ tách biệt đó, gộp về một locale duy nhất. Trade-off đáng vì consistency quan trọng hơn flexibility mà chắc chỉ có mỗi mình dùng.</p>
<p><strong>Tên app khi Siri gọi</strong>. Như đã thấy ở kịch bản 2 — Siri nhận diện app name không ổn định, đặc biệt khi user trộn Anh–Việt trong câu lệnh. Mình thử nhiều phrasing khác, kết quả tốt nhất hiện tại vẫn là: đừng tin Siri, dùng Shortcut.</p>
<p>Liên quan đến tên app nữa, mình đã thêm tên app theo locale: tiếng Việt là <code>Chi tiêu nhanh</code>, tiếng Anh vẫn là <code>QuickSpend</code>, còn tiếng Nhật, tiếng Tây Ban Nha nữa — không nhớ là gì, dùng translator dịch =)).</p>
<p>Với tên app như vậy, khi trigger bằng Siri sẽ có câu kiểu:</p>
<blockquote>
<p>"Hey Siri, thêm chi tiêu vào Chi tiêu nhanh."</p>
</blockquote>
<p>Nghe nó lủng củng sao sao ấy, nhưng chưa biết làm sao vì để Siri nhận diện thì vẫn phải kèm tên app.</p>
<h2>Phần kỹ thuật (skip OK nếu không quan tâm)</h2>
<p><p>Section này dành cho ai tò mò Apple stack. Không quan trọng để dùng feature — nhảy thẳng xuống
<a href="#t%E1%BB%95ng-k%E1%BA%BFt">phần tổng kết</a> cũng được.</p></p>
<p>Bốn thành phần chính:</p>
<ul>
<li><strong>App Intent (<code>AddExpenseIntent</code>)</strong> — define action với <code>@Parameter</code> text + <code>requestValueDialog</code> ("What expense should I add?"). Return type <code>IntentResult &#x26; ShowsSnippetView &#x26; ProvidesDialog</code> để Siri vừa đọc dialog vừa show snippet card. Đặt <code>openAppWhenRun = false</code> để happy path chạy hoàn toàn ngoài app — không launch UI, Siri đọc kết quả là xong.</li>
<li><strong><code>AppShortcutsProvider</code> + localized phrases</strong> — declare phrases EN với <code>\(.applicationName)</code> interpolation, các locale <code>vi/ja/es</code> seed vào <code>AppShortcuts.xcstrings</code>. <strong>Limitation đã nhắc ở trên</strong>: phrase chỉ accept parameter là <code>AppEntity</code>/<code>AppEnum</code>. Với free-form <code>String</code> thì phrase bắt buộc open-ended, Siri phải hỏi value qua dialog — đây là root cause của kịch bản 2 flaky.</li>
<li><strong>Snippet view + confirm flow</strong> — <code>ParsedExpenseSnippetView</code> là SwiftUI view render inline trong Siri card. Confidence ≥ 0.9 → <code>.result(dialog:)</code> auto-save, Siri đọc xác nhận, end flow. Confidence thấp → <code>requestConfirmation(actionName: .log, dialog:, snippet:)</code> ra card có nút Cancel / Log. Edge case khó chịu: label nút lấy từ system enum <code>ConfirmationActionName</code>, localize theo system locale chứ không theo app language — chưa fix được.</li>
<li><strong>Bundled Shortcut "Quick Expense"</strong> — chain <code>Dictate Text → AddExpenseIntent</code>, giúp user nói liền một mạch thay vì đợi Siri hỏi lại "nội dung gì". Đây là Shortcut cài qua link "Add to Shortcuts" ngay trong app.</li>
</ul>
<p>Code chi tiết hơn để dành bài khác — bài này dài quá thì các bạn lại skip mất.</p>
<h2>Tổng kết</h2>
<p>Sau patch 3.0, QuickSpend cuối cùng cũng đúng cái tên <code>Quick</code>:</p>
<ul>
<li>✅ <strong>Voice-first flow</strong>: log chi tiêu không cần mở app trong happy path.</li>
<li>✅ <strong>Shortcut safety net</strong>: luôn có path ổn định khi Siri trở chứng.</li>
<li>✅ <strong>AI parser auto-save</strong> khi confidence cao — Siri thông báo, không gián đoạn flow của user.</li>
</ul>
<p>Điểm chưa ổn:</p>
<ul>
<li>❌ Kịch bản "gọi thẳng app qua Siri" còn flaky. Mình đổ lỗi cho Siri, không nhận =)).</li>
<li>❌ Chưa test được với Siri có Apple Intelligence vì chưa lên đời máy xịn hơn iPhone 15. Bạn nào có iPhone 16 Pro hoặc 17 thử giúp mình, nếu Apple Intelligence cứu được kịch bản 2 thì mình sẽ rất biết ơn.</li>
</ul>
<p>Feature này thực sự hữu ích với chính mình — ba ngày qua data trong app full hơn cả tháng trước cộng lại. Nếu các bạn cũng dùng QuickSpend, update lên 3.0 khi nó ship và quẩy thử. Feedback gì cứ ib mình hoặc gửi qua form feedback trong app — mình đọc hết.</p>
<p>Còn nhiều thứ phải cải thiện trong luồng này:</p>
<ul>
<li>Cho user edit trực tiếp transaction trong UI confirm khi parser nhầm — thay vì chỉ save / cancel.</li>
<li>Polish prompt để parser thông minh hơn, handle được nhiều cách nói tự nhiên hơn của user.</li>
<li>Tìm thêm hướng tối ưu luồng — bớt step, bớt loading, bớt phụ thuộc Siri nếu có thể.</li>
<li>Khi có điều kiện nâng máy lên dòng có Apple Silicon hỗ trợ Apple Intelligence (iPhone 15 Pro trở lên), mình sẽ research <strong>Foundation Models framework</strong> (WWDC 2025) — Swift API gọi thẳng model ~3B on-device của Apple, có guided generation và tool calling. Hứa hẹn parser chạy hoàn toàn offline, không cần gọi cloud.</li>
</ul>
<p>Hy vọng sẽ có bài kế nữa về QuickSpend — nếu có người dùng thật thì mình mới có động lực update tiếp =)).</p>
<p>Hẹn gặp lại ở bài sau.</p>]]></content:encoded>
      <category>quickspend</category>
      <category>ios</category>
      <category>siri</category>
      <category>indie</category>
    </item>
    <item>
      <title>Hello world: Tai is calling</title>
      <link>https://taiphanvan.dev/vi/blog/hello-world</link>
      <guid isPermaLink="true">https://taiphanvan.dev/vi/blog/hello-world</guid>
      <pubDate>Thu, 07 May 2026 00:00:00 GMT</pubDate>
      <description>Sau nhiều năm lặng lẽ, mình cất tiếng. Canvas cá nhân — không gò 1 niche. Tech, AI, phim, sách, ảnh, game, bất cứ gì muốn ghi.</description>
      <content:encoded><![CDATA[<h2>Intro</h2>
<p>Bắt đầu một thứ luôn là rất khó, duy trì nó lại càng khó hơn. Mình thích làm điều khó nên mình làm trang này =)).</p>
<p>Có thể viết bài với nhiều người là điều bình thường nhưng với mình thì đây là một bước nhảy vọt. Mình sẽ cố gắng duy trì đều đặn, không để quá 2 tuần không có bài mới. Sẽ không hoàn hảo, nhưng ai cũng phải bắt đầu từ đâu đó.</p>
<p>Bắt đầu của mình là từ đây, bài viết đầu tiên, sau nhiều năm lặng lẽ quan sát, lắng nghe, giờ đến lúc cất tiếng, chào thế giới.</p>
<h2>Site này là gì</h2>
<p><code>taiphanvan.dev</code> không phải blog kỹ thuật thuần, cũng không phải portfolio. Đây là <strong>canvas cá nhân</strong> — chỗ mình tô vẽ mọi ý tưởng, ghi mọi thứ mình muốn ghi, làm mọi thứ mình muốn làm.</p>
<p>Lưu ý, mọi bài viết đều đậm tính cá nhân, nếu bạn thấy không hợp, có thể trao đổi với mình, mình luôn sẵn sàng mở rộng góc nhìn, tiếp cận vấn đề theo một góc khác — hoặc không, bạn có thể cứ lặng lẽ rời đi.</p>
<h2>Mình đang chuyển sang làm sản phẩm</h2>
<p>Trước giờ mình quen làm theo spec do người khác viết — thuần engineer. Mobile, web, vài năm chuyển stack, nhưng vai trò không đổi: nhận task, code, ship.</p>
<p>Cái mình đang đổi không phải stack — là <strong>vai trò</strong>. Mình muốn đứng từ đầu chuỗi: nhận diện vấn đề, design giải pháp, kiến trúc hệ thống, rồi mới đến code.</p>
<p>Gọi là gì cũng được — product owner, solution architect, system engineer — quan trọng là không gò bó vào 1 domain nữa. Mobile, web, backend, AI infra, devtools... ngon ở đâu thì làm ở đó. Stack chỉ là công cụ; vấn đề mới là cái cần giải.</p>
<h2>Sẽ viết gì</h2>
<p>Đối tượng đọc: bất cứ ai. Có thể là dev đang search debug log, có thể là bạn vô tình search "review phim X" rồi click vào.</p>
<p>Categories dự kiến:</p>
<ul>
<li><strong>tech</strong> — note từ build production, debug stories, pattern lặp lại</li>
<li><strong>ai</strong> — cách mình dùng LLM trong daily work, thí nghiệm, thất bại</li>
<li><strong>product</strong> — quyết định thiết kế, trade-off giữa "ngon" và "ship được"</li>
<li><strong>film, books</strong> — cái mình vừa xem, vừa đọc, đáng ghi lại</li>
<li><strong>photography</strong> — ảnh chụp + ghi chú thiết bị / setup</li>
<li><strong>games</strong> — game đang cày, vài cảm giác về game design</li>
<li><strong>thoughts</strong> — bất cứ thứ gì còn lại</li>
</ul>
<p>Không ép format. Một bài có thể 200 chữ, có thể 2000. Có quan điểm, không hedge. Sai thì sửa công khai.</p>
<h2>Roadmap</h2>
<p>Chưa có roadmap cụ thể về topic — đặt sẵn cứng dễ thành tự gò bản thân. Nhưng có cam kết về <strong>tần suất</strong>:</p>
<ul>
<li><strong>Target</strong>: 1 tuần 2 bài.</li>
<li><strong>Mandatory</strong>: 1 tuần ít nhất 1 bài.</li>
</ul>
<p>Tuần nào không có bài nào, coi như mình đang lười.</p>
<hr>
<p>Hẹn gặp lại bro ở bài sau.</p>]]></content:encoded>
      <category>meta</category>
      <category>personal</category>
      <category>indie</category>
    </item>
  </channel>
</rss>