Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
37df431
feat(forms): add filling agent type definitions
danielnaab Apr 19, 2026
61cd569
feat(forms): implement ScriptedFillingAgent
danielnaab Apr 19, 2026
950b06d
fix(forms): correct type declarations in ScriptedFillingAgent
danielnaab Apr 19, 2026
0037e18
feat(forms): implement SQLite conversation gateway
danielnaab Apr 19, 2026
332d1af
perf(forms): add index and stable ordering to conversation gateway
danielnaab Apr 19, 2026
db58950
feat(forms): implement system prompt builder for filling agent
danielnaab Apr 19, 2026
3ed4590
feat(forms): implement Bedrock-based LLM filling agent
danielnaab Apr 19, 2026
2682162
feat(ui): add chat panel component for conversational forms
danielnaab Apr 19, 2026
5052292
feat(ui): add client-side chat script for optimistic updates
danielnaab Apr 19, 2026
2c56b9f
feat(forms): add chat routes for conversational form filling
danielnaab Apr 19, 2026
c508c33
feat(forms): add view toggle between static and conversational modes
danielnaab Apr 19, 2026
7d3fbb2
feat(forms): wire conversational filling dependencies in server
danielnaab Apr 19, 2026
66ed24a
feat(fixtures): add conversational page to test fixture
danielnaab Apr 19, 2026
d14d149
test(forms): add integration test for conversational filling
danielnaab Apr 19, 2026
1bf0d07
chore: apply biome auto-fixes for formatting
danielnaab Apr 19, 2026
4793c70
fix(forms): add error handling for filling agent failures
danielnaab Apr 19, 2026
cc82175
fix(forms): use plain JSON Schema for Bedrock tool definitions
danielnaab Apr 19, 2026
ae09d40
chore: trigger deployment
danielnaab Apr 19, 2026
3e445e1
fix(forms): default to ScriptedFillingAgent until Bedrock tool schema…
danielnaab Apr 19, 2026
dbf69f8
fix(forms): wrap Bedrock tool schemas in json property
danielnaab Apr 19, 2026
6405791
fix(forms): use explicit type assertions in Bedrock tool schemas
danielnaab Apr 19, 2026
77b37d6
fix(forms): default to ScriptedFillingAgent pending Bedrock schema fix
danielnaab Apr 19, 2026
c27103b
feat(forms): improve ScriptedFillingAgent prompts to be more conversa…
danielnaab Apr 19, 2026
f50da25
fix(forms): use tool() helper with Zod schemas for Bedrock
danielnaab Apr 19, 2026
2c1ff58
fix(forms): default to BedrockFillingAgent now that tool() helper is …
danielnaab Apr 19, 2026
2d5302c
fix(forms): skip assistant messages with tool calls from history
danielnaab Apr 19, 2026
bdeeb20
fix(forms): prevent duplicate user messages in agent context
danielnaab Apr 19, 2026
4af3592
fix(forms): improve conversational agent guidelines
danielnaab Apr 19, 2026
de9b689
fix(forms): generate initial greeting when chat page loads
danielnaab Apr 19, 2026
ef65fe2
fix(forms): include full conversation history for agent context
danielnaab Apr 19, 2026
b48fbae
fix(forms): ensure agent always includes text with tool calls
danielnaab Apr 19, 2026
393448f
refactor(forms): migrate to flex-assistant for conversational UI
danielnaab Apr 19, 2026
b05c241
test(forms): update conversational tests for flex-assistant
danielnaab Apr 19, 2026
7d5e53b
fix(forms): constrain assistant panel to viewport height
danielnaab Apr 19, 2026
681059f
fix(forms): show assistant input by making parent flex container
danielnaab Apr 19, 2026
8e1861c
fix(forms): load flex-assistant component registration
danielnaab Apr 19, 2026
dfae340
fix(forms): adjust assistant panel positioning for header
danielnaab Apr 19, 2026
fc7f177
feat(forms): improve conversational mode UX
danielnaab Apr 19, 2026
1607569
fix(forms): fix client script loading for conversational chat
danielnaab Apr 19, 2026
2c7d7ef
fix(forms): fix Processing response and match editor layout
danielnaab Apr 19, 2026
fffb3bd
fix(forms): prepend user turn when history starts with assistant
danielnaab Apr 19, 2026
f93f328
chore(forms): add logging to bedrock agent for message ordering
danielnaab Apr 19, 2026
c0ad7d0
fix(forms): surface agent errors to client for debugging
danielnaab Apr 19, 2026
d11729b
Merge remote-tracking branch 'origin/main' into story-9/conversationa…
danielnaab Apr 19, 2026
15d11d9
chore: fix import ordering after merge
danielnaab Apr 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions scripts/build-components.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { copyFileSync } from 'node:fs'
import { mkdir } from 'node:fs/promises'
import { resolve } from 'node:path'

Expand All @@ -22,4 +23,13 @@ if (!result.success) {
process.exit(1)
}

// Copy standalone client scripts
copyFileSync(
resolve(
import.meta.dir,
'../src/entrypoints/app/public/conversational-form.js',
),
resolve(outdir, 'conversational-form.js'),
)

console.log('Components built to dist/components.js')
64 changes: 64 additions & 0 deletions src/design-system/components/flex-chat-panel/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { FC } from 'hono/jsx'

interface ConversationMessage {
id: string
role: 'user' | 'assistant'
content: string
createdAt: string
}

interface ChatPanelProps {
sessionId: string
messages: ConversationMessage[]
finished: boolean
}

export const ChatPanel: FC<ChatPanelProps> = ({
sessionId,
messages,
finished,
}) => {
return (
<div class="flex-chat-panel" data-session-id={sessionId}>
<div class="flex-chat-panel__messages">
{messages.map((message) => (
<div
key={message.id}
class="flex-chat-panel__message"
data-role={message.role}
>
<div class="flex-chat-panel__message-content">
{message.content}
</div>
</div>
))}
</div>

{!finished && (
<form class="flex-chat-panel__form" data-role="chat-form" method="post">
<div class="flex-chat-panel__input-group">
<textarea
class="flex-textarea"
name="message"
placeholder="Type your response..."
rows={3}
required
data-role="chat-input"
/>
</div>
<div class="flex-chat-panel__actions">
<button class="flex-button" type="submit">
Send
</button>
</div>
</form>
)}

{finished && (
<div class="flex-chat-panel__finished">
<p>Conversation complete</p>
</div>
)}
</div>
)
}
10 changes: 10 additions & 0 deletions src/design-system/components/flex-chat-panel/meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ComponentMeta } from '../../types'

export const meta: ComponentMeta = {
name: 'Chat Panel',
slug: 'flex-chat-panel',
category: 'form',
description: 'A conversational interface for interactive form filling.',
uswds: '',
interactive: true,
}
2 changes: 2 additions & 0 deletions src/design-system/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { meta as buttonGroup } from './components/flex-button-group/meta'
import { meta as card } from './components/flex-card/meta'
import { meta as changeIndicator } from './components/flex-change-indicator/meta'
import { meta as characterCount } from './components/flex-character-count/meta'
import { meta as chatPanel } from './components/flex-chat-panel/meta'
import { meta as checkbox } from './components/flex-checkbox/meta'
import { meta as collection } from './components/flex-collection/meta'
import { meta as comboBox } from './components/flex-combo-box/meta'
Expand Down Expand Up @@ -67,6 +68,7 @@ const components: ComponentMeta[] = [
card,
changeIndicator,
characterCount,
chatPanel,
checkbox,
collection,
comboBox,
Expand Down
7 changes: 7 additions & 0 deletions src/entrypoints/app/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Dev server entry point — builds CSS and components on startup and exports
* a Bun server config for --watch hot reload compatibility.
*/
import { copyFileSync } from 'node:fs'
import app from './server'

// Build CSS on startup
Expand All @@ -22,6 +23,12 @@ await Bun.build({
minify: false,
})

// Copy standalone client scripts to dist
copyFileSync(
'./src/entrypoints/app/public/conversational-form.js',
'./dist/conversational-form.js',
)

const port = process.env.PORT || 3000
console.log(`Server running on http://localhost:${port}`)

Expand Down
155 changes: 155 additions & 0 deletions src/entrypoints/app/public/chat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/**
* Chat panel client-side enhancement
*
* Provides optimistic UI updates and handles live chat responses:
* - Intercepts form submission
* - Appends user message immediately (optimistic)
* - POSTs to server with X-Live-Chat header
* - Appends assistant response when received
* - Auto-scrolls to bottom
* - Handles finished state
*/

;(() => {
// Find the chat panel on the page
const panel = document.querySelector('[data-session-id]')
if (!panel) {
return // No chat panel on this page
}

const form = panel.querySelector('[data-role="chat-form"]')
const input = panel.querySelector('[data-role="chat-input"]')
const messagesContainer = panel.querySelector('.flex-chat-panel__messages')

if (!form || !input || !messagesContainer) {
console.error('Chat panel: missing required elements')
return
}

/**
* Create a message bubble element
* @param {string} role - 'user' or 'assistant'
* @param {string} content - message text
* @returns {HTMLElement}
*/
function createMessageBubble(role, content) {
const messageDiv = document.createElement('div')
messageDiv.className = 'flex-chat-panel__message'
messageDiv.setAttribute('data-role', role)

const contentDiv = document.createElement('div')
contentDiv.className = 'flex-chat-panel__message-content'
contentDiv.textContent = content

messageDiv.appendChild(contentDiv)
return messageDiv
}

/**
* Scroll messages container to bottom
*/
function scrollToBottom() {
messagesContainer.scrollTop = messagesContainer.scrollHeight
}

/**
* Show finished state
*/
function showFinished() {
// Remove the form
form.remove()

// Add finished message
const finishedDiv = document.createElement('div')
finishedDiv.className = 'flex-chat-panel__finished'
const p = document.createElement('p')
p.textContent = 'Conversation complete'
finishedDiv.appendChild(p)
panel.appendChild(finishedDiv)
}

/**
* Handle form submission
* @param {Event} event
*/
async function handleSubmit(event) {
event.preventDefault()

const message = input.value.trim()
if (!message) {
return
}

// Disable form during submission
const submitButton = form.querySelector('button[type="submit"]')
input.disabled = true
if (submitButton) {
submitButton.disabled = true
}

try {
// Optimistically append user message
const userBubble = createMessageBubble('user', message)
messagesContainer.appendChild(userBubble)
scrollToBottom()

// Clear input
input.value = ''

// POST to server with X-Live-Chat header
const formData = new FormData()
formData.append('message', message)

const response = await fetch(form.action || window.location.pathname, {
method: 'POST',
headers: {
'X-Live-Chat': 'true',
},
body: formData,
})

if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}

const data = await response.json()

// Append assistant response
if (data.response) {
const assistantBubble = createMessageBubble('assistant', data.response)
messagesContainer.appendChild(assistantBubble)
scrollToBottom()
}

// Handle finished state
if (data.finished) {
showFinished()
return
}

// Re-enable form
input.disabled = false
if (submitButton) {
submitButton.disabled = false
}
input.focus()
} catch (error) {
console.error('Chat error:', error)
// Re-enable form on error
input.disabled = false
if (submitButton) {
submitButton.disabled = false
}
// TODO: Show error message to user
}
}

// Attach submit handler
form.addEventListener('submit', handleSubmit)

// Initial scroll to bottom (for server-rendered messages)
scrollToBottom()

// Focus input on load
input.focus()
})()
82 changes: 82 additions & 0 deletions src/entrypoints/app/public/conversational-form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Conversational form client-side enhancement
*
* Wires up flex-assistant component for conversational form filling:
* - Waits for custom element to be defined
* - Loads initial message history into assistant
* - Handles message submission events
* - POSTs messages to server with X-Live-Chat header
* - Displays responses in assistant panel
* - Reloads page when conversation is finished
*/

async function init() {
// Wait for the flex-assistant custom element to be defined
await customElements.whenDefined('flex-assistant')

const messagesScript = document.querySelector('[data-initial-messages]')
const assistant = document.querySelector('flex-assistant')

if (!messagesScript || !assistant) {
console.error('Missing required elements for conversational form')
return
}

// Parse and load initial messages
try {
const initialMessages = JSON.parse(messagesScript.textContent || '[]')
assistant.clearMessages()
for (const msg of initialMessages) {
assistant.addMessage(msg.role, msg.html)
}
} catch (error) {
console.error('Error loading initial messages:', error)
}

// Handle message submission
document.addEventListener('assistant:message-submitted', async (e) => {
const detail = e.detail
const text = detail.text

// Add user message to UI
assistant.addMessage('user', text)

// Send message to server
try {
const response = await fetch(window.location.pathname, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Live-Chat': 'true',
},
body: new URLSearchParams({ message: text }),
})

if (!response.ok) {
const errorText = await response.text()
assistant.addMessage(
'system',
`Error: ${response.status} - ${errorText}`,
)
return
}

const data = await response.json()

// Add assistant response
assistant.addMessage('assistant', data.response)

// If finished, reload to show completion state
if (data.finished) {
setTimeout(() => {
window.location.reload()
}, 1500)
}
} catch (error) {
console.error('Error sending message:', error)
assistant.addMessage('system', `Error: ${error.message}`)
}
})
}

init()
Loading
Loading