<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Natheesh Kumar</title>
    <description>The latest articles on DEV Community by Natheesh Kumar (@natheesh_kumar_8ad28dbe85).</description>
    <link>https://dev.to/natheesh_kumar_8ad28dbe85</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1546550%2F7ed7d740-7634-4902-9998-02879550c01d.jpg</url>
      <title>DEV Community: Natheesh Kumar</title>
      <link>https://dev.to/natheesh_kumar_8ad28dbe85</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/natheesh_kumar_8ad28dbe85"/>
    <language>en</language>
    <item>
      <title>I Finished What I Started: Adding AI to Every Layer of a Form Builder (With GitHub Copilot)</title>
      <dc:creator>Natheesh Kumar</dc:creator>
      <pubDate>Thu, 04 Jun 2026 17:58:15 +0000</pubDate>
      <link>https://dev.to/natheesh_kumar_8ad28dbe85/i-finished-what-i-started-adding-ai-to-every-layer-of-a-form-builder-with-github-copilot-942</link>
      <guid>https://dev.to/natheesh_kumar_8ad28dbe85/i-finished-what-i-started-adding-ai-to-every-layer-of-a-form-builder-with-github-copilot-942</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-05-21"&gt;GitHub Finish-Up-A-Thon Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Dculus Forms&lt;/strong&gt; is a full-stack, multi-tenant SaaS form builder — think Typeform, but with real-time collaborative editing (Y.js), a plugin system, analytics, and multi-cloud deployment.&lt;/p&gt;

&lt;p&gt;I started this project with the core builder working: drag-and-drop fields, a public form viewer, GraphQL API, Prisma + PostgreSQL. But several important layers were stubbed out or incomplete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The AI form builder chat existed but had stability problems: schema fetched fresh on every message, unbounded history, no user feedback while the AI was thinking, and no way to undo a bad AI edit.&lt;/li&gt;
&lt;li&gt;The analytics dashboard showed numbers but gave users no guidance on what to do about them.&lt;/li&gt;
&lt;li&gt;Dashboard stats had three hardcoded trend badges (&lt;code&gt;+12%&lt;/code&gt;, &lt;code&gt;+8%&lt;/code&gt;, &lt;code&gt;+15%&lt;/code&gt;) that were never connected to real data.&lt;/li&gt;
&lt;li&gt;The settings page was a double-nested tab maze.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The GitHub Finish-Up-A-Thon gave me the forcing function to finally ship these. Here's exactly what the before/after looks like — and how GitHub Copilot helped me get there faster than I thought possible.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmmrfddxfpd65atu1ep81.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmmrfddxfpd65atu1ep81.png" alt=" " width="799" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Before" State
&lt;/h2&gt;

&lt;h3&gt;
  
  
  AI Chat: Working but Fragile
&lt;/h3&gt;

&lt;p&gt;The form builder had an &lt;code&gt;AIEditDrawer&lt;/code&gt; — a chat panel where users could type "add a required email field after Full Name" and the AI would apply it. Under the hood: a Vercel AI SDK &lt;code&gt;ToolLoopAgent&lt;/code&gt; with 11 tools streaming over HTTP to a &lt;code&gt;useChat&lt;/code&gt; hook.&lt;/p&gt;

&lt;p&gt;Problems I'd deferred:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schema re-fetched from Y.js on every single message&lt;/strong&gt; — every turn read the full collaborative document regardless of whether anything had changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conversation history grew unbounded&lt;/strong&gt; — a long session could eat thousands of tokens just in history replay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Users saw generic bouncing dots&lt;/strong&gt; — no indication of what tool the AI was calling, what it changed, or how many steps it had taken.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No undo for AI turns&lt;/strong&gt; — if the AI made a bad batch edit, users had no targeted recovery path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Destructive actions applied immediately&lt;/strong&gt; — &lt;code&gt;removeFields&lt;/code&gt; went straight to the Y.js store with no warning, no "this affects 47 existing responses" heads-up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token quota was invisible&lt;/strong&gt; — users on the free plan (200k tokens/month) had no idea how close to the limit they were.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Analytics: Data Without Direction
&lt;/h3&gt;

&lt;p&gt;The field analytics tab showed per-field fill rates, option frequency charts, average response lengths, and completion time percentiles. Rich data — but purely descriptive. A user seeing "your Country dropdown has a 31% fill rate" had to mentally translate that into action themselves.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa7ot7uiwnl4f40oypfpt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa7ot7uiwnl4f40oypfpt.png" alt=" " width="800" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Dashboard Stats: Hardcoded Lies
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before — StatsGrid.tsx&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TrendBadge&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;positiveTrend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;  /&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;always&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TrendBadge&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;positiveTrend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;   /&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;always&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TrendBadge&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;positiveTrend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;  /&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;always&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;These weren't placeholders with a TODO comment. They had shipped and sat there for months.&lt;/p&gt;
&lt;h3&gt;
  
  
  Settings: Double-Nested Tab Maze
&lt;/h3&gt;

&lt;p&gt;The Settings page used &lt;code&gt;tabs within tabs&lt;/code&gt; — you'd click Account, then click a sub-tab for Profile, then another for Organization. Three levels of navigation for simple tasks.&lt;/p&gt;


&lt;h2&gt;
  
  
  How GitHub Copilot Helped Me Get There
&lt;/h2&gt;

&lt;p&gt;Before writing any feature code, I set up GitHub Copilot with domain-specific instruction files. This turned out to be the highest-leverage thing I did in the entire sprint.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Instruction Files
&lt;/h3&gt;

&lt;p&gt;I created 8 instruction files in &lt;code&gt;.github/instructions/&lt;/code&gt;, each scoped to a part of the codebase:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.github/instructions/
├── authentication.instructions.md   # better-auth patterns
├── backend.instructions.md          # resolver → service → repository layering
├── database.instructions.md         # Prisma conventions, serialization rules
├── frontend.instructions.md         # import rules, component organization
├── graphql.instructions.md          # schema-first conventions, error codes
├── i18n.instructions.md             # translation requirements (en + ta)
├── shared-packages.instructions.md  # @dculus/ui, @dculus/types, @dculus/utils
└── testing.instructions.md          # Vitest, Cucumber BDD patterns
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Each file has an &lt;code&gt;applyTo&lt;/code&gt; frontmatter that scopes it automatically. For example:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;applyTo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apps/form-app/**,apps/form-viewer/**,apps/admin-app/**"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;What this meant in practice:&lt;/strong&gt; When I asked Copilot to "add a GraphQL query for field insights," it automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Used &lt;code&gt;createGraphQLError&lt;/code&gt; with typed &lt;code&gt;GRAPHQL_ERROR_CODES&lt;/code&gt; (not &lt;code&gt;throw new Error()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Called &lt;code&gt;requireAuth&lt;/code&gt; + &lt;code&gt;requireOrganizationMembership&lt;/code&gt; in the resolver&lt;/li&gt;
&lt;li&gt;Used &lt;code&gt;@dculus/types&lt;/code&gt; for type imports, not local relative paths&lt;/li&gt;
&lt;li&gt;Added Tamil translations alongside English ones&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without the instruction files, I'd have been correcting import paths and missing auth guards on every single file. With them, the first draft was almost always structurally correct.&lt;/p&gt;
&lt;h3&gt;
  
  
  Custom Agents for Complex Work
&lt;/h3&gt;

&lt;p&gt;For multi-file features, I used Copilot's custom agent mode with specialized agents I'd defined:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;feature-developer&lt;/code&gt;&lt;/strong&gt; — full-stack features across GraphQL schema, resolver, service, and frontend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;field-type-developer&lt;/code&gt;&lt;/strong&gt; — covers all 10 layers a new field type touches (types, validation, renderer, builder UI, analytics, filters, export, collaboration, i18n)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When I was building AI Field Insights (a feature that spans Prisma schema, a new backend service, 2 GraphQL resolver additions, 5 frontend components, and translations), I used the &lt;code&gt;feature-developer&lt;/code&gt; agent with a detailed spec. Copilot produced a complete plan with a file map and step-by-step tasks, each with failing tests first. I reviewed, accepted the plan, and executed it task by task.&lt;/p&gt;

&lt;p&gt;The back-and-forth that would have taken me a day of context-switching took a focused afternoon.&lt;/p&gt;


&lt;h2&gt;
  
  
  What I Finished
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. AI Chat — Stability + Full Feature Overhaul
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Schema cache with 10-second TTL:&lt;/strong&gt;&lt;br&gt;
Instead of reading the Y.js document on every message, the route now caches the serialized form schema per &lt;code&gt;formId&lt;/code&gt;. A &lt;code&gt;/api/ai/invalidate-schema&lt;/code&gt; endpoint is called by &lt;code&gt;applyAIOp&lt;/code&gt; whenever a mutation is applied — so the cache is always fresh when the next message arrives.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// After — aiChat.ts (simplified)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schemaCache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormSchema&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;cachedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SCHEMA_CACHE_TTL_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getFormSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;FormSchema&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;schemaCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cachedAt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;SCHEMA_CACHE_TTL_MS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;schemaCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;20-message history cap:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// After — aiChatService.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_HISTORY_MESSAGES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aIChatMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;conversationId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;desc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;take&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MAX_HISTORY_MESSAGES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Token meter — users can now see their quota:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AITokenMeter.tsx — rendered at the bottom of every AIEditDrawer&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AITokenMeter&lt;/span&gt; &lt;span class="na"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;organizationId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;// Shows: [████████░░] 1.4M / 2M tokens used (Starter plan)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;&lt;code&gt;ChangeSummaryCard&lt;/code&gt; — what actually changed this turn:&lt;/strong&gt;&lt;br&gt;
Every AI response now includes a structured diff card showing added/modified/removed fields, so users don't have to visually scan the entire form to see what the AI did.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Propose-then-confirm for destructive operations:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;removeFields&lt;/code&gt;, &lt;code&gt;removePage&lt;/code&gt;, and &lt;code&gt;proposeFieldTypeChange&lt;/code&gt; no longer apply immediately. They return a proposal card showing exactly how many existing responses would be affected:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⚠️  Remove "Country" (select)?
    This field has 312 existing responses.
    [Cancel]  [Confirm Delete]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Field type conversion done right:&lt;/strong&gt;&lt;br&gt;
Converting a text field to a select isn't a mutation — it's delete-old + create-new with a new ID. This preserves response history integrity (old responses keep their text values; the new field starts clean). Copilot caught this edge case when I described the feature: it flagged that mutating &lt;code&gt;type&lt;/code&gt; in place would corrupt the &lt;code&gt;data: { [fieldId]: value }&lt;/code&gt; structure that analytics and exports rely on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context-aware suggestion chips (&lt;code&gt;useAIChips&lt;/code&gt;):&lt;/strong&gt;&lt;br&gt;
The chat drawer now shows smart chips based on the current state — "Add validation", "Reorder fields", "Add a page" — derived from the form's actual content, not hardcoded strings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Net result:&lt;/strong&gt; 14 tools total (up from 11), all with TypeScript Zod schemas validated at runtime, streamed via &lt;code&gt;DefaultChatTransport&lt;/code&gt;.&lt;/p&gt;


&lt;h3&gt;
  
  
  2. AI Field Insights — Analytics That Explains Itself
&lt;/h3&gt;

&lt;p&gt;This is the feature I'm most proud of, because it closes the loop between &lt;strong&gt;seeing&lt;/strong&gt; a problem and &lt;strong&gt;fixing&lt;/strong&gt; it without leaving the page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User opens Form Analytics → Field Analytics.&lt;/li&gt;
&lt;li&gt;No tips yet — a &lt;strong&gt;"✨ Analyze all fields"&lt;/strong&gt; button appears.&lt;/li&gt;
&lt;li&gt;One click triggers a single batch AI call (1–3k tokens). The AI receives all field analytics data and returns a structured insight per field.&lt;/li&gt;
&lt;li&gt;Tips are persisted in a new &lt;code&gt;AIFieldInsight&lt;/code&gt; Prisma model, keyed by &lt;code&gt;(formId, fieldId)&lt;/code&gt; with a &lt;code&gt;schemaHash&lt;/code&gt; for staleness detection.&lt;/li&gt;
&lt;li&gt;Future visits read from the database — zero token cost until the form schema changes.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model AIFieldInsight {
  id           String   @id @default(cuid())
  formId       String
  fieldId      String
  tip          String   @db.Text      // "67% of users skip this field. Consider making it optional."
  fixPrompt    String   @db.Text      // exact message to pre-fill in AIEditDrawer
  severity     String                 // "warning" | "error" | "success" | "info"
  schemaHash   String                 // 16-char SHA-256 prefix — staleness detection
  generatedAt  DateTime @default(now())

  form Form @relation(fields: [formId], references: [id], onDelete: Cascade)

  @@unique([formId, fieldId])
  @@index([formId])
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;The "Fix with AI ✦" button:&lt;/strong&gt;&lt;br&gt;
Each insight card has a button that opens the &lt;code&gt;AIEditDrawer&lt;/code&gt; with the &lt;code&gt;fixPrompt&lt;/code&gt; pre-loaded as the initial message. The user clicks confirm, and the AI applies the suggested fix immediately. The gap between insight and action is one click.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stale detection:&lt;/strong&gt;&lt;br&gt;
If the form schema changes after analysis, a yellow banner appears: &lt;em&gt;"Form changed since last analysis — Re-analyze?"&lt;/em&gt; This is powered by comparing a 16-char SHA-256 prefix of the field structure against the stored &lt;code&gt;schemaHash&lt;/code&gt;.&lt;/p&gt;


&lt;h3&gt;
  
  
  3. Real Trend Percentages — Goodbye Hardcoded Lies
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TrendBadge&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;positiveTrend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;  /&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="nx"&gt;fiction&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TrendBadge&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;positiveTrend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;   /&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;  &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="nx"&gt;fiction&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TrendBadge&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;positiveTrend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt;  /&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="nx"&gt;fiction&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;After — GraphQL schema:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;FormDashboardStats&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;responsesToday&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;responsesThisWeek&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;responsesThisMonth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;trendResponsesToday&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Float&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c"&gt;# % change today vs yesterday; null = no data&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;trendThisWeek&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Float&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="c"&gt;# % change this week vs last week; null = no data&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;trendResponseRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Float&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="c"&gt;# percentage-point delta; null = &amp;lt;10 views&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The resolver runs 4 parallel Prisma queries and computes real period-over-period deltas. &lt;code&gt;null&lt;/code&gt; means insufficient data — the badge simply hides instead of showing a misleading number. A form with 3 total responses doesn't pretend it grew by 12%.&lt;/p&gt;


&lt;h3&gt;
  
  
  4. Settings Redesign — Typeform-Style Sidebar
&lt;/h3&gt;

&lt;p&gt;Replaced double-nested tabs with a clean left sidebar following a pattern users already know from tools like Typeform and Linear:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Settings
├── Account
│   └── Profile          ← name, email, avatar upload
└── Organization
    ├── Members           ← invite, role management
    └── Billing &amp;amp; Plans   ← Chargebee subscription dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Old URLs (&lt;code&gt;/settings/account&lt;/code&gt;, &lt;code&gt;/settings/team&lt;/code&gt;) redirect to new equivalents — no broken links for existing users.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Copilot Moment That Surprised Me Most
&lt;/h2&gt;

&lt;p&gt;When building the field type conversion feature, I described the problem to Copilot: "convert a text field to a select field." Its first response wasn't code — it was a question about data integrity:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If you mutate the &lt;code&gt;type&lt;/code&gt; in place and keep the same field ID, responses stored as &lt;code&gt;data: { [fieldId]: "some text" }&lt;/code&gt; will be read by the select renderer expecting an option value. Do you want conversion to create a new field ID so old responses are preserved as immutable history?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the kind of thing I would have caught in a code review — after a bug report from a user. Copilot caught it during design.&lt;/p&gt;

&lt;p&gt;The instruction files were doing their job: Copilot knew that &lt;code&gt;response.data&lt;/code&gt; is &lt;code&gt;JSONB&lt;/code&gt; keyed by &lt;code&gt;fieldId&lt;/code&gt;, that analytics and exports all read from that structure, and that changing a field's type mid-life would silently corrupt historical data. It had the full mental model of the system because I'd written it down.&lt;/p&gt;


&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;React 18, Vite, TypeScript, Tailwind CSS, shadcn/ui&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI&lt;/td&gt;
&lt;td&gt;Vercel AI SDK v5, Azure OpenAI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State&lt;/td&gt;
&lt;td&gt;Apollo Client (server), Zustand slices (local)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Realtime&lt;/td&gt;
&lt;td&gt;Y.js + Hocuspocus WebSocket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Express.js, Apollo Server (GraphQL SDL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;PostgreSQL + Prisma ORM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;better-auth (cookie + bearer, org plugin)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Billing&lt;/td&gt;
&lt;td&gt;Chargebee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;Vitest (unit), Playwright + Cucumber (E2E)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Azure Container Apps (backend) + Cloudflare Pages (frontend)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h2&gt;
  
  
  GitHub Repository
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/dculussoftwares/dculus-forms" rel="noopener noreferrer"&gt;github.com/dculussoftwares/dculus-forms&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.github/instructions/&lt;/code&gt; folder, custom agents, and skills are all in the repo — feel free to use the pattern for your own project.&lt;/p&gt;




&lt;h2&gt;
  
  
  Demo URL:
&lt;/h2&gt;
&lt;h2&gt;
  
  
  &lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://form-app-dev.dculus.com/" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;form-app-dev.dculus.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;

&lt;/h2&gt;


&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Write the context down first, then write the code.&lt;/strong&gt; The 8 instruction files took about 2 hours to write and saved me 10x that in correction loops. Every time Copilot produced something architecturally wrong without them, it was because I hadn't told it how this specific codebase works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Specs as prompts.&lt;/strong&gt; I wrote design specs in &lt;code&gt;docs/superpowers/specs/&lt;/code&gt; before writing a line of code. These double as context for Copilot — feeding it a spec file produces dramatically better first drafts than "hey, build me a thing."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The schema cache story is a Copilot story.&lt;/strong&gt; I described the performance problem (schema read on every message). Copilot suggested the TTL cache + invalidation endpoint pattern, explained why a push invalidation is better than a TTL-only approach for a real-time collaborative document, and wrote the first draft. I reviewed it, tweaked the TTL, and shipped it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with GitHub Copilot. Finished for the &lt;a href="https://dev.to/challenges/github-2026-05-21"&gt;GitHub Finish-Up-A-Thon Challenge&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
      <category>githubcopilot</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
